01_Controller, Service, Repository 3단 분리
📍 기존 Controller 문제점
하나의 Controller에서 3가지 역할을 수행하고 있다.
controller/user/UserController
@RestController
public class UserController {
(중략)
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequest request) {
String readSql = = "SELECT * FROM user WHERE id = ?";
boolean isUserNotExist = jdbcTemplate.query(readSql, (rs, rowNum) -> 0, request.getId()).isEmpty();
if (isUserNotExist) {
throw new IllegalArgumentException();
}
String updateSql = "UPDATE user SET name = ? WHERE id = ?";
jdbcTemplate.update(updateSql, request.getName(), request.getId());
}
}
- @PutMapping("/user") : API의 진입 지점으로써 HTTP Body를 객체로 변환 -> Controller
- if (isUserNotExist) : 현재 유저가 있는지 확인하고 예외 처리 -> Service
- String readSql, updateSql : SQL을 사용해 실제 DB와 통신 -> Repository
📍 PUT API 3단 분리
기존 UserController의 updateUser 메소드의 3가지 역할을 각각 Controller, Service, Repository로 분리한다.

Controller
API의 진입 지점으로써 HTTP Body를 객체로 변환한다.
UserService 클래스의 updateUser를 호출하려면 UserService를 필드로 가지고 있어야 한다.
controller/user/UserController
@RestController
public class UserController {
private final JdbcTemplate jdbcTemplate;
private final UserService userService;
public UserController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.userService = new UserService(jdbcTemplate);
}
(중략)
@PutMapping("/user")
public void updateUser(@RequestBody UserUpdateRequest request) {
userService.updateUser(request);
}
}
Service
현재 유저가 있는지 확인하고 예외를 처리한다.
service/user/UserService
public class UserService {
private final UserRepository userRepository;
public UserService(JdbcTemplate jdbcTemplate) {
this.userRepository = new UserRepository(jdbcTemplate);
}
public void updateUser(UserUpdateRequest request) {
if (userRepository.isUserNotExist(request.getId())) {
throw new IllegalArgumentException();
}
userRepository.updateUserName(request.getName(), request.getId());
}
}
Repository
DB에 SQL을 날리는 역할, 저장장치와의 접근을 담당한다.
JdbcTemplate를 변수로 갖고, 생성자를 만들면 파라미터로 전달받지 않아도 된다.
repository/user/UserRepository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public boolean isUserNotExist(long id) {
String sql = "SELECT * FROM user WHERE id = ?";
return jdbcTemplate.query(sql, (rs, rowNum) -> 0, id).isEmpty();
}
public void updateUserName(String name, long id) {
String sql = "UPDATE user SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, name, id);
}
}
📍 DELETE API 3단 분리
Controller
controller/user/UserController
@RestController
public class UserController {
private final JdbcTemplate jdbcTemplate;
private final UserService userService;
public UserController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.userService = new UserService(jdbcTemplate);
}
(중략)
@PutMapping("/user")
public void updateUser(@RequestParam String name) {
userService.deleteUser(name);
}
}
Service
service/user/UserService
public class UserService {
(중략)
public void deleteUser(String name) {
if (userRepository.isUserNotExist(name)) {
throw new IllegalArgumentException();
}
userRepository.deleteUser(name);
}
}
Repository
repository/user/UserRepository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public boolean isUserNotExist(String name) {
String readSql = "SELECT * FROM user WHERE name = ?";
return jdbcTemplate.query(readSql, (rs, rowNum) -> 0, name).isEmpty();
}
public void deleteUser(String name) {
String sql = "DELETE FROM user WHERE name = ?";
jdbcTemplate.update(sql, name);
}
}
📍 POST API 3단 분리
Controller
controller/user/UserController
@RestController
public class UserController {
private final JdbcTemplate jdbcTemplate;
private final UserService userService;
public UserController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.userService = new UserService(jdbcTemplate);
}
(중략)
@PostMapping("/user")
public void saveUser(@RequestBody UserCreateRequest request) {
userService.saveUser(request);
}
}
Service
service/user/UserService
public class UserService {
(중략)
public void saveUser(UserCreateRequest request) {
userRepository.saveUser(request.getName(), request.getAge());
}
}
Repository
repository/user/UserRepository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void saveUser(String name, Integer age) {
String sql = "INSERT INTO user (name, age) VALUES (?, ?)";
jdbcTemplate.update(sql, name, age);
}
}
📍 GET API 3단 분리
Controller
controller/user/UserController
@RestController
public class UserController {
private final JdbcTemplate jdbcTemplate;
private final UserService userService;
public UserController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
this.userService = new UserService(jdbcTemplate);
}
(중략)
@GetMapping("/user")
public List<UserResponse> getUsers() {
return userService.getUsers();
}
}
Service
service/user/UserService
public class UserService {
(중략)
public List<UserResponse> getUsers() {
return userRepository.getUserResponses();
}
}
Repository
repository/user/UserRepository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<UserResponse> getUserResponses() {
String sql = "SELECT * FROM user";
return jdbcTemplate.query(sql, (rs, rowNum) -> {
long id = rs.getLong("id");
String name = rs.getString("name");
int age = rs.getInt("age");
return new UserResponse(id, name, age);
});
}
}
02_스프링 컨테이너와 스프링 빈
📍 Controller와 JdbcTemplate
@RestController
public class UserController {
private final UserService userService;
public UserController(JdbcTemplate jdbcTemplate) {
this.userService = new UserService(jdbcTemplate);
}
}
🔍 코드 설명
- @RestController : 클래스를 API의 진입 지점으로 만들어주고, 이 클래스를 스프링 빈으로 등록시킨다.
⁉️ 의문점
- 클래스 안에 있는 함수를 사용하기 위해서는 인스턴스화가 필요한데 UserController를 현재 인스턴스화하고 있지 않다.
- JdbcTemplate를 처리하지 않고 사용하고 있다.
-> 스프링 컨테이너는 UserController를 인스턴스화할 때, 컨트롤러가 필요로 하는 JdbcTemplate을 컨테이너 내부에서 찾아 인스턴스화를 진행해준다.
💡 서버 시작 이후
- 스프링 컨테이너 시작(@SpringBootApplication이 자동으로 설정해줌)
- 컨테이너에 여러 스프링 빈 등록됨(ex. JdbcTemplate)
- 개발자가 설정해준 스프링 빈 등록됨(ex. UserController)
- 필요한 의존성 자동 설정됨(ex. UserController 만들 때 JdbcTemplate 넣어줌)
📍 스프링 빈
스프링이 대신 만들어서 관리해주는 객체
스프링 빈 등록
Repository를 스프링 빈으로 등록하면 JdbcTemplate을 바로 가져올 수 있다.
repository/user/UserRepository
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
🔍 코드 설명
- @Repository : Repository를 스프링 빈으로 등록해준다.
service/user/UserService
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
🔍 코드 설명
- @Service : Service를 스프링 빈으로 등록해준다.
controller/user/UserController
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
📍 스프링 컨테이너
객체(빈)를 대신 만들어 주고, 필요한 곳에 자동으로 연결해준다.
Service 코드를 전혀 바꾸지 않고 repository만을 온전하게 변경하고 싶을 때 사용할 수 있다.
- 제어의 역전(IoC, Inversion of Control) : 객체의 생성과 의존관계 설정을 개발자가 직접 하지 않고, 스프링 컨테이너가 대신 관리하도록 제어권을 넘기는 것
- 의존성 주입(DI, Dependency Injection) : IoC를 구현하는 방법 중 하나로, 객체가 직접 의존 대상을 생성하지 않고 외부(스프링 컨테이너)로부터 주입받는 방식
@Primary
우선권을 제어할 수 있다.
여러 repository 중 원하는 repository에 붙여서 service를 만든다.
@Service, @Repository
개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 사용한다.
@Configuration, @Bean
외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때
- @Configuration
클래스에 붙이는 어노테이션
@Bean을 사용할 때 함께 사용해 주어야 한다.
- @Bean
메소드에 붙이는 어노테이션
메소드에서 반환되는 객체를 스프링 빈에 등록한다.
@Configuration
public class UserConfiguration {
@Bean
public UserRepository userRepository(JdbcTemplate jdbcTemplate) {
return new UserRepository(jdbcTemplate);
}
}
@Component
주어진 클래스를 컴포넌트로 간주한다.
이 클래스들은 스프링 서버가 뜰 때 자동으로 감지된다.
@RestController / @Service / @Repository / @Configuration 은 모두 @Component 어노테이션을 가지고 있다.
컨트롤러, 서비스, 리포지토리가 모두 아니고 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용되기도 한다.
@Qualifier
스프링 빈을 사용하는 쪽, 스프링 빈을 등록하는 쪽 모두 @Qualifier를 사용할 수 있다.
스프링 빈을 사용하는 쪽에서만 쓰면, 빈의 이름을 적어주어야 한다.
양 쪽 모두 사용하면, @Qualifier끼리 연결된다.
@Primary와 @Qualifier 중에서는 사용하는 쪽에서 직접 적어준 @Qualifier가 이긴다.
- 스프링 빈을 주입받는 방법
1. 생성자 사용(가장 권장!)
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
// 생성자에 JdbcTemplate이 있으므로 스프링 컨테이너가 넣어준다.
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
2. setter 사용 : 누군가 setter를 사용하면 오작동할 수 있다.
@Repository
public class UserRepository {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
3. 필드에 바로 주입 : 테스트를 어렵게 만드는 요인이다.
@Repository
public class UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
}'Back-end > SpringBoot' 카테고리의 다른 글
| [SpringBoot] Chap 2 - 도서 관리 서비스 DB 조작하기 (0) | 2026.01.19 |
|---|---|
| [SpringBoot] Chap 1 - 도서 관리 서비스 API 생성하기 (0) | 2026.01.18 |
| [SpringBoot] 5장 게시글 조회(Read) (0) | 2026.01.15 |
| [SpringBoot] 3-4장 게시글 생성(CREATE), 롬복(lombok)과 리팩터링 (0) | 2026.01.11 |
| [SpringBoot] 1-2장 환경설정, MVC 패턴, 레이아웃 (0) | 2026.01.08 |
