Controller: "누가 요청했어? 무슨 데이터가 필요해?" → API 요청을 받는 문지기
Service: "오케이, 이 요청 처리해줄게!" → 핵심 로직 처리 담당
Repository: "데이터 줘! 저장도 해줘!" → DB와 소통하는 영업사원
🧱 각각 뭐하는 앤데?
1. 🎮 Controller — API의 입구이자 전면 배너
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}
클라이언트가 /api/users/1 호출하면 여기로 들어옴.
요청 파라미터 확인하고, 서비스에 “요거 처리 좀” 요청함.
✅ Tip: @RestController = JSON 응답을 반환하는 컨트롤러
2. 🧠 Service — 진짜 ‘일’하는 핵심 로직 덩어리
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserDto getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("유저 없음!"));
return new UserDto(user);
}
}
DB에 직접 접근은 안 해요! Repository한테 시켜요~
예외 처리, 데이터 가공, 트랜잭션 처리 등 실질적인 비즈니스 로직 담당!
✅ Tip: @Service 붙이면 스프링이 빈으로 관리해줌 (IoC/DI)
3. 🗃️ Repository — 진짜 DB랑 통신하는 데이터 엔지니어
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 커스텀 쿼리도 만들 수 있어요
Optional<User> findByEmail(String email);
}
JPA를 통해 CRUD 메서드를 자동으로 제공받음
DB에게 “user 테이블에서 이거 가져와!”라고 요청하는 역할
✅ Tip: JpaRepository 덕분에 findAll(), save(), delete() 같은 메서드는 이미 다 있음.
스프링부트는 복잡한 설정 없이도 효율적인 구조로 개발할 수 있게 도와줘요. 하지만 구조의 의도를 정확히 이해하고 써야 실무에서도 "개발자 맛집" 되는 거죠 🍜
설정 > 에디터 > 파일 인코딩 > 프로퍼티 파일에 대한 디폴트 인코딩을 UTF-8로 저장 후 아래 체크박스를 체크한후 확인
2-3) 프로퍼티 파일 설정
main > resources > application.properties 파일에 설정을 추가합니다.
# 서버 포트
server.port=8000
# 오라클 설정 : log4j 적용
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:오라클서버포트/db명
spring.datasource.username=서버id
spring.datasource.password=서버패스워드
# jpa 설정
spring.jpa.hibernate.ddl-auto=none
#spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
spring.jpa.show-sql=true
# sql log 찍기
spring.jpa.properties.hibernate.format_sql=true
#Logging Setting , hibernate info 레벨 로깅 설정 : debug, trace 등
logging.level.org.hibernate=info
# batch size 설정 : 연관관계 설정 시 N+1 문제 최소화
# 여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어줌
spring.jpa.properties.hibernate.default_batch_fetch_size=1000
# 1) resource/data.sql 자동 실행 ( DML 실행 )
# -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.jpa.defer-datasource-initialization=true
# 2) resource/data.sql 자동 실행 ( DML 실행 )
# -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always
# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true
# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true
# HikariCP settings
spring.datasource.hikari.minimumIdle=1
spring.datasource.hikari.maximumPoolSize=1
spring.datasource.hikari.poolName=HikariPoolBooks
package com.example.simpledms.model;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import javax.persistence.*;
/**
* packageName : com.example.modelexam.model
* fileName : Dept
* author : kangtaegyung
* date : 2022/10/12
* description : 부서 모델 클래스
* 요약 :
* Hard Delete : 실제 데이터를 삭제하는 행위
* Soft Delete는 물리적인 데이터 삭제로 발생할 수 있는 문제를 방지하고 쉽게 복원할 필요가 있거나 삭제된 데이터들을 보관하여 데이터로써 활용할 필요나 가치가 있는 경우에 사용
* 실무에서는 법적으로 개인자료일 경우 3년 또는 1년이상 데이터를 보관할 의무가 있고 어길수 법적 문제가 생길 수 있음 -> 그래서 soft delete 방식을 대부분 구현하고 있음
* ===========================================================
* DATE AUTHOR NOTE
* -----------------------------------------------------------
* 2022/10/12 kangtaegyung 최초 생성
*/
@Entity
@Table(name="TB_DEPT")
@SequenceGenerator(
name = "SQ_DEPT_GENERATOR"
, sequenceName = "SQ_DEPT"
, initialValue = 1
, allocationSize = 1
)
@Getter
@Setter
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DynamicInsert
@DynamicUpdate
// soft delete
// 사용법 : 1) @Where(clause = "DELETE_YN = 'N'") : select 될 때 조건("DELETE_YN = 'N'")을 강제로 붙여줌
// 2) @SQLDelete(sql = "대체 sql문") : delete 될때 대체해서 실행될 쿼리문
@Where(clause = "DELETE_YN = 'N'")
@SQLDelete(sql = "UPDATE TB_DEPT SET DELETE_YN = 'Y', DELETE_TIME=TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') WHERE DNO = ?")
public class Dept extends BaseTimeEntity {
// 부서넘버
// @Id : Primary Key 에 해당
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE
, generator = "SQ_DEPT_GENERATOR"
)
@Column
private Integer dno;
// 부서이름
@Column
private String dname;
// 부서위치
@Column
private String loc;
}
/** dname(부서명) like 검색 + dname 내림차순 조회 */
public List<Dept> findAllByDnameContainingOrderByDnameDesc(String dname){
List<Dept> list
= deptRepository.findAllByDnameContainingOrderByDnameDesc(dname);
return list;
}
📂 DeptController.java 추가
/** 전체조회 + dname(부서명) like 검색 + dname 내림차순 조회 */
@GetMapping("/dept/dname/containing/desc/{dname}")
public ResponseEntity<Object> findAllByDnameContainingOrderByDnameDesc(
@PathVariable String dname
){
try {
// todo : 전체 조회 + 정렬(dno 내림차순)함수 호출
List<Dept> list
= deptService
.findAllByDnameContainingOrderByDnameDesc(dname);
if (list.isEmpty() == false) {
// 성공
return new ResponseEntity<>(list, HttpStatus.OK);
} else {
// 데이터 없음
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e) {
log.debug(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
✔ 서버 재시작 후 API 테스트
1) 전체조회 - 부서명 like 검색 + dname 내림차순 조회
✅ TODO 4) 쿼리메소드 응용 연습문제
// todo) 응용연습
// todo: 응용 연습문제
// todo: 연습 4) EMP 테이블에서 Job 이 manager 이고
// 매개변수로 부서번호(dno)를 받는 함수를 작성하세요.
// todo: 연습 5) Emp 테이블에서 salary 가 1000 ~ 1500 사이의 값을 같는
// 사원을 조회하려고 합니다. 함수를 작성해 주세요
List<Emp> findAllByJobAndDno(String job, int dno);
List<Emp> findAllBySalaryBetween(int first, int last);
📂 EmpService.java
// todo: 연습 4) EMP 테이블에서 Job 이 manager 이고
// 매개변수로 부서번호(dno)를 받는 함수를 작성하세요.
public List<Emp> findAllByJobAndDno(String job, int dno){
List<Emp> list = empRepository.findAllByJobAndDno(job, dno);
return list;
}
// todo: 연습 5) Emp 테이블에서 salary 가 1000 ~ 1500 사이의 값을 같는
// 사원을 조회하려고 합니다. 함수를 작성해 주세요
public List<Emp> findAllBySalaryBetween(int first, int last){
List<Emp> list = empRepository.findAllBySalaryBetween(first, last);
return list;
}
📂 EmpController.java
/** 연습 4) : 쿼리메소드 */
@GetMapping("/emp/dno/{dno}")
public ResponseEntity<Object> findAllByEnameContaining(
@PathVariable int dno
) {
try {
// 전체 조회 + 정렬(dno 내림차순) 호출
List<Emp> list
= empService.findAllByJobAndDno("MANAGER", dno);
if (list.isEmpty() == false) {
// 성공
return new ResponseEntity<>(list, HttpStatus.OK);
} else {
// 데이터 없음
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e){
log.debug(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/** 연습5 */
@GetMapping("emp/salary/{first}/{last}")
public ResponseEntity<Object> findAllBySalaryBetween(@PathVariable int first, @PathVariable int last){
try {
List<Emp> list
= empService.findAllBySalaryBetween(first, last);
if(list.isEmpty() == false){
return new ResponseEntity<>(list, HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e) {
log.debug(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
데이터베이스에서 값을 가져올 때는 위의 코드처럼 메서드의 이름만으로 쿼리 메서드를 생성할 수 도 있습니다.
이번에는 @Query 어노테이션을 사용해 직접 JPQL을 작성해봅시다.
📂 DeptRepository.java
// ----------------------------------------
// @Query 예제
// 1) 오라클 쿼리
// 2) 객체 쿼리
// ----------------------------------------
// todo 1) ename like 검색
@Query(value = "SELECT TD.* FROM TB_EMP TD WHERE TD.ENAME LIKE '%' || :dname || '%'", nativeQuery = true)
List<Emp> selectByEname(@Param("ename") String ename);
위 코드를 다르게 작성하는 방법(JDK 버전에 따라 사용 불가능할 수도 있음)
// todo: 1-1) 위의 코드를 다르게 코딩
@Query(value = "SELECT TD.* FROM TB_DEPT TD WHERE TD.DNAME LIKE '%'||:dname||'%'",
nativeQuery = true)
List<Dept> selectByDname(String dname);
// todo: 1-2) 위의 코드를 다르게 코딩 : 참고
@Query(value = "SELECT TD.* FROM TB_DEPT TD WHERE TD.DNAME LIKE '%'|| ?1 ||'%'",
nativeQuery = true)
List<Dept> selectByDname(String dname);
✅ SQL에서 쿼리문 결과가 올바른지 확인
📂 DeptService.java
/** 전체조회 + dname like 검색 : @Query */
public List<Dept> selectByDname(String dname) {
List<Dept> list
= deptRepository.selectByDname(dname);
return list;
}
📂 DeptController.java
/** 전체조회 + dname like 검색 : @Query */
@GetMapping("/dept/dname/{dname}")
public ResponseEntity<Object> selectByDname(
@PathVariable String dname
) {
try {
// 전체 조회 + 정렬(dno 오름차순) 호출
List<Dept> list
= deptService.selectByDname(dname);
if (list.isEmpty() == false) {
// 성공
return new ResponseEntity<>(list, HttpStatus.OK);
} else {
// 데이터 없음
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
} catch (Exception e){
log.debug(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
그리고 main > resources > application.properties 파일에 설정을 추가합니다.
# 서버 포트
server.port=8000
# todo : docker db 설정
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
# todo : spring.datasource.url=jdbc:log4jdbc:oracle:thin:@ip주소:db포트번호/db이름
spring.datasource.url=jdbc:log4jdbc:oracle:thin:@localhost:1521/xepdb1
spring.datasource.username=scott
spring.datasource.password=!Ds1234567890
# todo : jpa 설정
# todo : ddl-auto - create(모든 테이블 재생성)/update(수정된 테이블만 생성)/none(안함)
spring.jpa.hibernate.ddl-auto=create
#spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto=none
# todo : db 제품 연결 ( oracle 12 이상 : Oracle12cDialect )
spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
# todo : generate-ddl=true (테이블 자동 생성 옵션)
spring.jpa.generate-ddl=true
# todo : sql log 보기 (true/false)
spring.jpa.show-sql=true
# 1) resource/data.sql 자동 실행 ( DML 실행 )
# -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
# todo : DML, DDL 스크립트 (실습용 샘플 테이블) 실행을 위한 옵션
spring.jpa.defer-datasource-initialization=true
# todo : sql log 좀 더 정리된 형태로 찍기
spring.jpa.properties.hibernate.format_sql=true
# todo : 로깅 레벨 : error < info < debug < trace (정보 많은 순서)
logging.level.org.hibernate=info
# 2) resource/data.sql 자동 실행 ( DML 실행 )
# -> data.sql ( dml 실행 ), schema.sql ( ddl 실행 )
spring.sql.init.mode=always
# sql 에러 무시하고 스프링 서버 로딩
spring.sql.init.continue-on-error=true
# 자바 소스 변경시 스프링 서버 자동 재시작
spring.devtools.restart.enabled=true
# PUT , DELETE 방식도 form 에서 사용할 수 있게 만들어줌 : jsp에서 사용함
spring.mvc.hiddenmethod.filter.enabled=true
DROP SEQUENCE SQ_DEPT;
CREATE SEQUENCE SQ_DEPT START WITH 50 INCREMENT BY 10;
DROP SEQUENCE SQ_EMP;
CREATE SEQUENCE SQ_EMP START WITH 8000 INCREMENT BY 1;
DROP SEQUENCE SQ_FAQ;
CREATE SEQUENCE SQ_FAQ START WITH 1 INCREMENT BY 1;
DROP TABLE TB_EMP CASCADE CONSTRAINT;
DROP TABLE TB_DEPT CASCADE CONSTRAINT;
DROP TABLE TB_FAQ CASCADE CONSTRAINT;
CREATE TABLE TB_DEPT (
DNO NUMBER NOT NULL PRIMARY KEY,
DNAME VARCHAR2(255),
LOC VARCHAR2(255),
INSERT_TIME VARCHAR2(255),
UPDATE_TIME VARCHAR2(255)
);
CREATE TABLE TB_EMP
(ENO NUMBER NOT NULL PRIMARY KEY,
ENAME VARCHAR2(255),
JOB VARCHAR2(255),
MANAGER NUMBER,
HIREDATE VARCHAR2(255),
SALARY NUMBER,
COMMISSION NUMBER,
DNO NUMBER,
INSERT_TIME VARCHAR2(255),
UPDATE_TIME VARCHAR2(255)
);
CREATE TABLE TB_FAQ (
NO NUMBER NOT NULL PRIMARY KEY,
TITLE VARCHAR2(255),
CONTENT VARCHAR2(255),
INSERT_TIME VARCHAR2(255),
UPDATE_TIME VARCHAR2(255)
);
그리고 java > com.example.jpaexam 폴더 아래에 다음과 같이 폴더를 생성합니다.
💡 model 폴더에 공통모델 파일 생성
model 폴더 아래에 생성일자/수정일자를 만들어주는 추상클래스를 생성합니다.
실습에서는 파일명을 BaseTimeEntity 로 생성하였습니다.
📂 model > BaseTimeEntity.java
1) 먼저 추상클래스 위에 어노테이션 3개를 추가합니다.
package com.example.jpaexam.model;
import lombok.Getter;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
/**
* packageName : com.example.jpaexam.model
* fileName : BaseTimeEntity
* author : GGG
* date : 2023-10-16
* description : JPA 에서 자동으로 생성일자/수정일자를 만들어주는 클래스
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-16 GGG 최초 생성
*/
@Getter
// todo : 자동으로 생성일자/수정일자 컬럼을 sql 문에 추가시키는 어노테이션 2개
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
// todo) 공통 속성
private String insertTime;
private String updateTime;
}
여기서 @MappedSuperclass 어노테이션의 의미는
생성일자/수정일자를 모든 엔티티에 공통으로 가져가야하는 상황에서 위의 코드처럼
BaseTimeEntity를 정의해서 활용할 때 사용합니다.
2) 공통 속성의 형태를 변경하는 함수 추가
위의 코드는 time의 형태가 yyyy-MM-dd HH:mm:ss 형태가 아닌 기본 형태로 보이게 됩니다.
가독성을 좋게하기 위해 공통속성 코드 아래에 함수를 추가합니다.
...
private String updateTime;
// todo) 해당 테이블에 데이터가 만들어 질 때(insert 문) 실행되는 이벤트 함수
@PrePersist
void OnPerPersist(){
this.insertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
// todo) 해당 테이블에 데이터가 수정될 때(update 문) 실행되는 이벤트 함수
@PreUpdate
void OnPreUpdate(){
this.updateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
this.insertTime = this.updateTime; // 생성일시 == 수정일시 동일하게 처리
}
Hibernate:
select
dept0_.dno as dno1_0_,
dept0_.insert_time as insert_time2_0_,
dept0_.update_time as update_time3_0_,
dept0_.dname as dname4_0_,
dept0_.loc as loc5_0_
from
tb_dept dept0_
앞 전 예제에서는 쿼리스트링 방식으로 매개변수를 전달하였습니다. 이번에는 쿼리스트링 방식을 개선한 파라미터 방식의 매개변수 전달 방법에 대해 알아보겠습니다.
📂PathVariableController.java
@Controller
@RequestMapping("/exam05")
public class PathVariableController {
// TODO : url 테스트 파라미터 방식 : http://localhost:8000/exam05/path-variable/LeeJunHyuk
// => @GetMapping("/path-variable/{웹매개변수명}")
// => 웹 브라우저 주소창 사용 : url/값
// TODO : url 테스트 쿼리스트링 방식 : http://localhost:8000/exam05/path-variable?name=LeeJunHyuk
@GetMapping("/path-variable/{name}")
public String getPathVariable(
@PathVariable String name,
Model model)
{
model.addAttribute("name", name);
return "exam05/path_variable.jsp";
}
}
// @Setter : setter 함수를 만들어주는 어노테이션
// @Getter : getter 함수를 만들어주는 어노테이션
// @ToString : toString 재정의 함수 자동으로 만들어주는 어노테이션
// @AllArgsConstructor : 모든 속성을 가진 생성자 자동 정의 어노테이션
먼저 실습을 하기 위해 아래와 같이 폴더를 구성합니다.
com.example.controllerexam 폴더 아래에 model 폴더를 생성합니다.
📂 model > Member.java
package com.example.controllerexam.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
@AllArgsConstructor
public class Member {
// TODO : 속성, 생성자 함수(getter/setter)
String id; // 회원 id
String name; // 회원 이름
}
Lombok 어노테이션을 사용함으로써 코드가 간결해집니다.
📂 ObjectParamController.java
package com.example.controllerexam.controller.exam04;
import com.example.controllerexam.model.Member;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* packageName : com.example.controllerexam.controller.exam04
* fileName : ObjectParamController
* author : GGG
* date : 2023-10-05
* description : @ModelAttribute : 객체 형태로 변환하는 어노테이션
* 요약 :
* <p>
* ===========================================================
* DATE AUTHOR NOTE
* —————————————————————————————
* 2023-10-05 GGG 최초 생성
*/
@Controller
@RequestMapping("/exam04")
public class ObjectParamController {
// TODO : URL TEST ) http://localhost:8000/exam04/object-param?id=junyeoke&name=lee
// @ModelAttribute : 객체의 속성명으로 jsp로 각각 전달
// Member 클래스의 속성명 == url의 매개변수명 : id, name
@GetMapping("/object-param")
public String getObjectParam(
@ModelAttribute Member member){
return "exam04/object_param.jsp";
}
}
@ModelAttribute는 사용자가 요청시 전달하는 값을 오브젝트 형태로 매핑해주는 어노테이션입니다.