섹션 4. 역할의 분리와 스프링 컨테이너

김민지·2025년 3월 23일

서버 기초 강의

목록 보기
4/12

좋은 코드(Clean Code)는 왜 중요한가?!

좋은 코드 vs 나쁜 코드

  1. 나쁜 코드: 불명확한 변수명 등으로 인해 해당 함수가 어떤 역할을 수행하는 것인지 한눈에 보기 어려움
public void good(U u){if (u.a <= 60 && u.a > 10 && u.b > 130) { c();}}
public class U {int a; int b;}
  1. 좋은 코드: 코드만 읽어도 대략적으로 어떤 일이 일어나고 있는 건지 알 수 있음
public void getOnTheRideIfPossible(User user) {
  if (user.canGetOnTheRide()) {
ride(); }
}
public class User {
  private int age;
  private int height;
  public boolean canGetOnTheRide() {
    return this.age > 10 && this.age <= 60 && this.height > 130;
} }

우리가 작성한 controller

@PutMapping("/user") // PUT /user
    public void updateUser(@RequestBody UserUpdateRequest request) {
        // 수정하고자 하는 User가 데이터베이스에 존재하는지 검증
        String sqlCheck = "SELECT * FROM user WHERE id = ?";
        // user가 존재하지 않는다면 빈 리스트가 반환될 것임.
        boolean isUserNotExist = jdbcTemplate.query(sqlCheck, (rs, rowNum) -> 0, request.getId()).isEmpty();
        if (isUserNotExist) {
            throw new IllegalArgumentException();
        }

        String sql = "UPDATE user SET name = ? WHERE id = ?";
        jdbcTemplate.update(sql, request.getName(), request.getId());
    }
  • 위의 코드에는 아래 세가지 기능이 동시에 포함되어있으므로, 각 기능별로 분리할 필요가 있음
  1. API의 진입 지점으로, HTTP body를 객체로 변환(DTO)
  2. 요청 중인 사용자가 존재하는지 아닌지를 검증하고 예외처리
  3. SQL을 사용해서 데이터베이스와 통신

3단 분리하기: Controller, Service, Repository

  • Controller : API의 진입 지점으로, HTTP body를 객체로 변환(DTO) 하는 역할만 수행
    @PutMapping("/user") // PUT /user
    public void updateUser(@RequestBody UserUpdateRequest request) {
        userService.updateUser(jdbcTemplate, request);
    }
  • Service : 요청 중인 사용자가 존재하는지 아닌지를 검증하고 예외처리하는 역할을 수행
package com.group.libraryapp.service.user;

import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.repository.user.UserRepository;
import org.springframework.jdbc.core.JdbcTemplate;

public class UserService {
    private final UserRepository userRepository;

    public UserService(JdbcTemplate jdbcTemplate) {
        userRepository = new UserRepository(jdbcTemplate);
    }

    public void updateUser(UserUpdateRequest request) {
        if (userRepository.isUserNotExist(request.getId())) {
            throw new IllegalArgumentException();
        }
        userRepository.updateUserName(request.getName(), request.getId());
    }
}
  • Repository : SQL을 사용해서 데이터베이스와 통신하는 역할을 수행
package com.group.libraryapp.repository.user;

import org.springframework.jdbc.core.JdbcTemplate;

public class UserRepository {
    private final JdbcTemplate jdbcTemplate;

    public UserRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public boolean isUserNotExist(long id) {
        String sqlCheck = "SELECT * FROM user WHERE id = ?";
        return jdbcTemplate.query(sqlCheck, (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);
    }
}
  • 이 때, JdbcTemplate은 데이터베이스에 접근할 때만 필요한데, 이걸 controller에서 선언하면 계속 파라미터로 넘겨서 내려줘야하는 불편함이 있으므로

UserController와 스프링 컨테이너

지금까지 작성한 코드의 의문점들:

  1. 클래스 안의 함수를 사용하기 위해서는 인스턴스화(new 클래스)가 필요하다. 그러나 우리는 현재 클래스를 인스턴스화 하지 않았다. 이게 어떻게 가능한걸까?

  2. UserController의 생성자는 JdbcTemplate라는 클래스를 필요로 한다(의존한다). 그러나 우리는 JdbcTemplate에 대해 한번도 처리한 적이 없음에도 가져와서 사용했다.

결론적으로, 이 두가지의 동작은 @RestController 덕분이다.

@RestController 는 해당 클래스를 API의 진입지점으로 만들어줄 뿐 아니라, 해당 클래스를 Spring Bean으로 등록시킨다.

클래스를 Spring Bean으로 등록시킨다는 건 무슨 뜻일까?

@SpringBootApplication을 main 클래스 위에 붙이면 서버 실행에 필요한 이런저런 설정들을 자동으로 해준다고 앞에서 이해했었는데, 이 과정을 조금 더 구체적으로 생각해보자.

우리가 인텔리제이에서 시작 버튼을 눌러서 main 클래스를 작동시키면, @SpringBootApplication은 자동으로 스프링 서버 내부에 컨테이너를 생성한다. 이렇게 생성된 서버 내부에는 스프링 빈으로 등록된 클래스들이 들어가있다. 이 때, 해당 클래스들의 인스턴스화도 이루어진다.

  • Spring Bean 에 대해 조금 더 알아보자.

    아래의 다이어그램에서 jdbcTemplate를 더블 클릭하면 jdbcTemplate 관련 코드가 나타난다.

    해당 코드는 아래와 같이 spring-boot-autoconfigure:2.7.6에 위치해있다.

    ...

이런 코드를 우리가 어떻게 추가한 걸까? build.gradle 파일의 dependencies 파트를 확인해보면 우리가 추가한 것을 확인할 수 있다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // 얘가 jdbcTemplate를 가져와줌
	implementation 'org.springframework.boot:spring-boot-starter-web'
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'mysql:mysql-connector-java'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
  • 그렇다면 왜 UserRepository는 JdbcTemplate를 가져오지 못할까?
    JdbcTemplate를 가져오려면 UserRepository가 Spring bean이어야 하는데, UserRepository는 Spring bean이 아님

  • 즉, UserRepository에서 JdbcTemplate을 바로 가져오기 위해서는 해당 클래스를 Spring Bean으로 만들어줘야 함.
    이를 위해서는 @Repository라는 어노테이션을 붙여줌.

Spring Bean으로 등록하기 위한 방법

계층어노테이션
Controller 계층@RestController
Repository 계층@Repository
Service 계층@Service

스프링 컨테이너를 왜 사용할까?!

  • 스프링 컨테이너: 서버가 시작될 때 함께 시작되는 클래스들을 담는 거대한 공간
  • 스프링 빈: 스프링 컨테이너에 담긴 클래스 하나하나

요구사항: 책 이름을 메모리(DB 사용하지 않음)에 저장하는 API 구현하기 (단, Service, Repository 는 스프링 빈이 아니어야 함)

  • Service, Repository 등을 스프링빈으로 등록하지 않았다고 가정하자.
    이 때, 초반에는 데이터베이스를 사용하지 않고 로컬에만 저장해서 Repository 에서 ArrayList 등을 활용해 저장했다고 하자.
    추후에 로직을 확장하면서 이제는 데이터베이스를 활용하겠다고 결정했다고 하면
    (1) 데이터베이스에 연결하는 레포지토리를 추가하고,
    (2) Service 에서 private final <Repository명> <repository명> = new <Repository명>() 등 레포지토리를 불러오는 부분을 수정해줘야 한다.
    ⮕ 지금은 한 군데에서만 Repository를 가져다 사용했지만 실제 로직에서는 100군데, 1000군데에서 가져다 사용할 수 있다.
    그러면 데이터 저장 로직만 변경했는데 1000여군데의 코드를 연달아 수정해줘야 하며, 이 과정에서 오류가 발생할 위험도 있다.

partial solution

부분적인 해결방법으로는, 자바의 interface 를 활용하는 것이다. Service 폴더 내에 interface 타입으로 해서 BookRepository 이런 코드를 만들어주고, 아래와 같이 수정해준다.

public class BookService {
	private final BookRepository bookRepository = new BookMemoryRepository(); 
    // 이후에 BookMemoryRepository() 부분만 바꿔주면 됨
}
public interface BookRepository {
	public void save(String bookName);
}
public class BookMemoryRepository implements BookRepository {
	private final List<String> books = new ArrayList();
	@Override
	public void save(String bookName) {
		books.add(bookName);
	}
}
public class BookMySqlRepository implements BookRepository {
	@Override
	public void save(String bookName) {
	// jdbcTemplate.....
	}
}

물론 수정해야 하는 부분이 적어지기는 했지만 여전히 Service 코드를 불러와 사용한 부분마다 찾아가서 바꿔줘야 한다는 사실은 변하지 않는다.

스프링 컨테이너를 활용한 최종 해결책

  • 스프링 컨테이너를 사용하면 컨테이너가 Service 코드를 대신 인스턴스화 하고, 그때 알아서 Repository를 결정함
    컨테이너가 interfaceextends 한 클래스 중 하나를 선택하는 방식을 제어의 역전 (IoC, Inversion of Control)이라 부른다.
  • 이때 컨테이너가 interfaceextends 한 클래스 중 하나를 선택해서 넣어주는 과정을 의존성 주입(Dependency Injection)이라고 한다.
  • 우리는 무엇을 넣어줄 지를 @Primary 어노테이션을 이용해 우선권을 제어할 수 있다
@Service
public class BookService {
	private final BookRepository bookRepository;
	public BookService(BookRepository bookRepository) {
		this.bookRepository = bookRepository;
	}
}

public interface BookRepository {
	public void save(String bookName);
}

@Repository
public class BookMemoryRepository implements BookRepository {
	@Override
	public void save(String bookName) {
		println("Memory Repository " + bookName);
	}
}

@Repository
@Primary // 우선권을 부여하는 어노테이션!!
public class BookMySqlRepository implements BookRepository {
	@Override
	public void save(String bookName) {
		println("MySQL Repository " + bookName);
	}
}

스프링 컨테이너를 다루는 방법

스프링 빈을 등록하는 방법

  1. @Configuration (클래스에 붙이는 어노테이션으로, Bean을 사용할 때 같이 사용해줘야 함), @Bean(메소드에 붙이는 어노테이션으로, 메소드에서 반환되는 객체를 컨테이너에 등록함)
  2. @Service, @Repository
  3. @Component: 주어진 클래스를 컴포넌트로 간주하여 이 클래스들은 컨테이너가 뜰 때마다 자동으로 감지되고, 스프링빈이 되게 됨. (@Service, @Repository, @RestController 에도 내장되어있음)
    컨트롤러, 서비스, 레포지토리가 모두 아닌, 개발자가 직접 작성한 클래스를 스프링 빈으로 등록해야할 때 사용함

스프링 빈을 주입받는 방법

  1. (가장 권장) 생성자를 사용해 주입받는 방식: 예를 들어 컨트롤러에서 서비스를 주입받아야 하는경우, 아래와 같이 생성자를 추가해서 받아옴
@RestController
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) { // 생성자 추가
        this.userService = userService;
    }
  1. setter(), @Autowired 사용하기: 누군가 setter()를 사용할 위험이 있음
@RestController
public class UserController {
    // setter 방식
    private UserService userService;
    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
  1. 필드에 직접 @Autowired 사용하기: 테스트가 어려워짐

@Qualifier vs @Primary

  • @Qualifier > @Primary

0개의 댓글