Spring Boot 애플리케이션의 관심사 분리와 의존성 주입 완벽 가이드

1. 계층화 아키텍처(Layered Architecture)

Spring Boot 기반 웹 애플리케이션은 일반적으로 세 가지 주요 계층으로 구성됩니다:

  • 컨트롤러(Controller) 계층: 클라이언트의 HTTP 요청을 수신하고, 비즈니스 로직 처리를 서비스 계층에 위임합니다. 처리 결과를 다시 클라이언트에 반환하는 역할을 담당합니다.
  • 서비스(Service) 계층: 핵심 비즈니스 로직이 구현되는 곳입니다. 데이터 접근이 필요한 경우 데이터 액세스 객체(DAO) 계층을 호출하여 필요한 데이터를 조회하거나 변경합니다.
  • 데이터 액세스(DAO) 계층: 데이터베이스나 파일 등 데이터 저장소와 직접 통신하며, 데이터의 생성, 조회, 수정, 삭제(CRUD) 작업을 수행합니다. 영속성 계층(Persistence Layer)이라고도 불립니다.

2. 결합도(Coupling)와 분리(Decoupling)

2.1 결합도란?

소프트웨어 설계 원칙 중 중요한 개념은 높은 응집도(High Cohesion)와 낮은 결합도(Loose Coupling)입니다.

  • 응집도: 하나의 모듈(클래스) 내부 요소들이 얼마나 밀접하게 관련되어 있는지를 나타냅니다. 예를 들어, EmpService 클래스는 직원 관련 로직만 처리해야 높은 응집도를 가집니다.
  • 결합도: 서로 다른 계층이나 모듈 간의 의존성 정도를 나타냅니다. 결합도가 높으면 한 모듈의 변경이 다른 모듈에 연쇄적인 영향을 미칩니다.

결합도가 높은 코드의 예: Service 구현체를 변경할 때마다 Controller의 코드도 수정해야 하는 상황이 발생합니다.

2.2 결합도 분리 방법

기존 방식은 필요한 객체를 직접 new 키워드로 생성했습니다. 이는 계층 간 강한 결합을 유발합니다. 이를 해결하기 위한 핵심 개념은 다음과 같습니다:

  • IoC (Inversion of Control, 제어의 역전): 객체의 생성 및 생명주기 관리를 개발자가 직접 하지 않고 컨테이너에게 위임하는 설계 원칙입니다. 이 컨테이너를 IoC 컨테이너 또는 Spring 컨테이너라고 부릅니다.
  • DI (Dependency Injection, 의존성 주입): 애플리케이션이 실행 중 필요로 하는 의존 객체(리소스)를 IoC 컨테이너가 외부에서 주입해주는 방식입니다. 예를 들어, EmpControllerEmpService 객체를 필요로 할 때, Spring 컨테이너가 해당 객체를 생성하여 필드나 생성자 등을 통해 주입합니다.

IoC 컨테이너가 관리하는 객체를 빈(Bean)이라고 합니다.

3. Spring IoC & DI 실전 적용

3.1 실습 예제: 직원 목록 조회

Controller, Service, Dao 계층 간의 결합도를 해결하는 과정입니다.

1단계: Controller와 Service에서 new 키워드를 제거합니다.

2단계: Service와 Dao 구현체에 @Component 애너테이션을 추가하여 IoC 컨테이너에 빈으로 등록합니다.
3단계: Controller와 Service에 @Autowired 애너테이션을 사용하여 필요한 의존성을 주입받습니다.

컨트롤러 계층:

@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/employees")
    public Result listEmployees() {
        List<Employee> employees = employeeService.getAllEmployees();
        return Result.success(employees);
    }
}

서비스 계층:

@Component
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeDao employeeDao;

    @Override
    public List<Employee> getAllEmployees() {
        List<Employee> employeeList = employeeDao.findAll();

        // 데이터 변환 로직 (성별, 직무 등)
        employeeList.forEach(emp -> {
            // 성별 변환 (1 -> 남, 2 -> 여)
            if ("1".equals(emp.getGender())) {
                emp.setGender("남성");
            } else if ("2".equals(emp.getGender())) {
                emp.setGender("여성");
            }

            // 직무 변환 (1 -> 강사, 2 -> 담임, 3 -> 취업지도)
            switch (emp.getJob()) {
                case "1": emp.setJob("강사"); break;
                case "2": emp.setJob("담임"); break;
                case "3": emp.setJob("취업지도"); break;
            }
        });
        return employeeList;
    }
}

데이터 액세스 계층:

@Component
public class EmployeeDaoImpl implements EmployeeDao {
    @Override
    public List<Employee> findAll() {
        // emp.xml 파일을 파싱하여 Employee 객체 리스트 반환
        String filePath = this.getClass().getClassLoader().getResource("employees.xml").getFile();
        return XmlParserUtils.parse(filePath, Employee.class);
    }
}

3.2 IoC 상세 설명

IoC 컨테이너가 관리하는 빈(Bean)을 더 명확하게 구분하기 위해, Spring은 @Component의 파생 애너테이션을 제공합니다.

  • @Controller: 컨트롤러 계층 클래스에 사용합니다. (@RestController@Controller + @ResponseBody의 조합입니다.)
  • @Service: 비즈니스 로직을 담당하는 서비스 계층 클래스에 사용합니다.
  • @Repository: 데이터 액세스 계층(DAO) 클래스에 사용합니다. (MyBatis 등과 통합 시 잘 사용되지 않을 수 있습니다.)
  • @Component: 위 세 가지 범주에 속하지 않는 일반적인 빈(예: 유틸리티 클래스)을 등록할 때 사용합니다.
애너테이션 설명 적용 위치
@Controller @Component의 파생, 웹 요청 처리 컨트롤러 클래스
@Service @Component의 파생, 비즈니스 로직 처리 서비스 클래스
@Repository @Component의 파생, 데이터 액세스 처리 DAO 클래스
@Component 기본 빈 등록 애너테이션 기타 클래스
3.2.1 컴포넌트 스캔(Component Scan)

빈으로 등록된 클래스가 실제로 IoC 컨테이너에 의해 발견되려면 컴포넌트 스캔이 이루어져야 합니다. @SpringBootApplication 애너테이션은 기본적으로 해당 애너테이션이 위치한 클래스의 패키지와 모든 하위 패키지를 스캔 대상으로 설정합니다.

만약 스캔 범위를 벗어난 패키지에 빈이 있다면, @ComponentScan 애너테이션을 메인 클래스에 추가하여 스캔 범위를 명시적으로 지정할 수 있으나, 공식적인 권장 방식은 아닙니다.

3.3 DI 상세 설명

DI는 IoC 컨테이너가 애플리케이션 실행에 필요한 의존 객체(빈)를 제공하는 메커니즘입니다. 하지만 IoC 컨테이너에 동일한 타입의 빈이 여러 개 존재하면 어떤 빈을 주입해야 할지 모호해져 오류가 발생할 수 있습니다. 이 문제를 해결하기 위한 방법은 세 가지입니다.

  • @Primary: 동일한 타입의 빈이 여러 개일 때, 우선적으로 주입할 빈에 이 애너테이션을 추가합니다.
  • @Qualifier: 주입받을 빈의 이름을 명시적으로 지정합니다. @Autowired와 함께 사용해야 합니다. 예: @Autowired @Qualifier("employeeServiceImpl")
  • @Resource: Java 표준 애너테이션으로, 빈의 이름(name)을 기준으로 주입합니다. @Resource(name = "employeeServiceImpl")

면접 질문: @Autowired@Resource의 차이점은?

  • @Autowired는 Spring 프레임워크에서 제공하는 애너테이션이며, 타입(Type)을 기준으로 의존성을 주입합니다.
  • @Resource는 Java 표준(JSR-250) 애너테이션이며, 이름(Name)을 기준으로 의존성을 주입합니다.

태그: SpringBoot IOC DI 의존성주입 계층분리

7월 9일 04:19에 게시됨