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 컨테이너가 외부에서 주입해주는 방식입니다. 예를 들어,
EmpController가EmpService객체를 필요로 할 때, 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)을 기준으로 의존성을 주입합니다.