계층 분리란?
계층 분리란 애플리케이션의 책임과 역할을 명확히 구분하여 각 부분이 독립적으로 동작하도록 설계하는 방식입니다.
계층의 일반적인 구성
Presentation Layer (Controller):
- 사용자 또는 클라이언트로부터 요청을 받아들이고 응답을 처리합니다.
- 비즈니스 로직이나 데이터베이스와 직접적으로 상호작용하지 않고, Service Layer를 호출하여 요청을 처리합니다.
- API의 경우 HTTP 요청(예: GET, POST)을 처리하며, UI에서는 사용자 인터페이스와의 상호작용을 관리합니다.
Business Logic Layer (Service):
- 애플리케이션의 핵심 비즈니스 로직을 처리합니다.
- 데이터를 가공하거나 여러 데이터 저장소와 상호작용하여 특정한 작업을 수행합니다.
Data Access Layer (Repository):
- 데이터베이스, 파일 시스템 등과 상호작용하여 CRUD(Create, Read, Update, Delete) 작업을 처리합니다.
- 데이터를 저장, 조회, 삭제 등의 작업을 제공합니다.
- 데이터 저장소와 관련된 구체적인 구현을 캡슐화합니다.
특정 객체에 종속되지 않는 설계
종속성이란?
- 특정 계층이 다른 계층의 세부 구현(특정 클래스)에 의존하면, 그 구현이 변경되었을 때 영향을 받습니다.
종속성의 특징과 문제점
강한 결합 (Tight Coupling)
- 한 계층이나 모듈이 다른 특정 계층의 세부 구현에 직접 의존하는 경우.
- 문제점:
- 데이터 접근 방식(예: JPA에서 MongoDB로 변경)이 바뀌면 Controller와 다른 계층 코드까지 수정해야 합니다.
- 시스템의 변경 요구 사항이 발생했을 때 전체 시스템의 많은 부분이 영향을 받습니다.
취약한 구조
- 특정 기술, 프레임워크, 또는 라이브러리에 종속적인 설계.
- 예를들어 Repository 계층에서 데이터베이스 접근 기술로 JPA를 사용 중인데, 프로젝트 요구 사항이 변경되어 NoSQL(MongoDB)을 사용해야 할 경우 기존 로직을 모두 수정해야 합니다.
재사용성 저하
- 강한 종속성으로 인해 특정 모듈이나 클래스가 다른 환경에서 재사용되기 어려움.
- 예를들어 Service Layer가 특정 데이터베이스의 스키마나 구조에 강하게 의존하는 경우, 동일한 Service Layer를 다른 프로젝트에 재사용하기 어렸습니다.
테스트 어려움
- 테스트 환경에서도 종속성은 문제를 일으킬 수 있음.
- 예를들어 Repository가 실제 데이터베이스에 연결된 상태로 테스트하려면 데이터베이스가 반드시 필요하며, 테스트가 느려지고 복잡해집니다
종속성을 줄이는 방법
인터페이스 사용
- 계층 간 의존성을 인터페이스로 추상화하여 세부 구현과 분리합니다.
- Service는 Repository 인터페이스에만 의존하고, 구현체는 나중에 연결합니다.
- 구현체를 교체하더라도 인터페이스는 변하지 않으므로 다른 계층에는 영향이 없습니다.
// Repository 인터페이스
public interface UserRepository {
User findById(Long id);
void save(User user);
}
// JPA를 사용한 구현체
@Repository
public class JpaUserRepository implements UserRepository {
@Override
public User findById(Long id) { // JPA 코드 }
@Override
public void save(User user) { // JPA 코드 }
}
// Service는 인터페이스에만 의존
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository; // 구현체 주입
}
public User getUser(Long id) {
return userRepository.findById(id);
}
}
의존성 주입 (Dependency Injection)
- 의존성 주입(DI)은 객체의 의존성을 직접 생성하지 않고 외부에서 주입받는 방식입니다.
- Spring에서는 생성자 주입을 통해 구현체를 주입합니다.
장점
- Service는 구현체가 무엇인지 알 필요 없이 인터페이스만 알면 됩니다.
- 쉽게 Mock 객체로 대체할 수 있어 테스트가 용이합니다.
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
계층 간 결합도 낮추기
- 결합도는 소프트웨어 설계에서 한 모듈(계층)이 다른 모듈에 얼마나 강하게 의존하는가를 나타내는 척도입니다.
- 결합도가 낮을수록 각 모듈은 독립적으로 변경, 테스트, 배포가 가능하며 유지보수가 쉬워집니다.
// Controller는 Service에만 의존
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(new UserDto(user));
}
}
// Service는 Repository에만 의존
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id).orElseThrow(
() -> new RuntimeException("User not found"));
}
}
확장성과 테스트 용이성
새로운 데이터베이스 추가
- UserRepository를 통해 추상화되어 있기 때문에, JPA 대신 MongoDB를 사용하는 구현체를 추가할 수도 있습니다.
- Service나 Controller는 변경하지 않아도 됩니다.
@Repository
public class MongoUserRepository implements UserRepository {
@Override
public User findById(Long id) { // MongoDB 코드 }
@Override public void save(User user) { // MongoDB 코드 }
}
2) Mock 객체를 활용한 테스트
- 구현체 대신 Mock 객체를 주입하여 테스트를 진행할 수 있습니다.
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock private UserRepository userRepository;
@InjectMocks private UserService userService;
@Test void testGetUserById() {
// Mock 데이터 설정
User mockUser = new User(1L, "Test User");
when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// 테스트 실행
User result = userService.getUserById(1L);
// 검증
assertEquals("Test User", result.getName());
}
}
계층 분리를 통해 얻는 이점
- 유지보수성 (Maintainability)
- 코드가 명확히 분리되어 있어 특정 계층만 수정해도 나머지 부분에 영향을 주지 않습니다.
- 예를들어 데이터베이스를 MySQL에서 PostgreSQL로 바꿀 때 Repository Layer만 수정하면 됩니다.
- 재사용성 (Reusability)
- Service Layer는 Presentation Layer와 독립적이기 때문에, 동일한 비즈니스 로직을 웹, 모바일, API 등 다양한 인터페이스에서 재사용 가능합니다.
- 테스트 용이성 (Testability)
- 각 계층을 독립적으로 테스트할 수 있어 유닛 테스트와 통합 테스트 작성이 용이합니다.
- Mock 객체를 사용해 Service나 Controller를 테스트할 수 있습니다.
- 변경 용이성 (Ease of Change)
- UI/UX, 비즈니스 로직, 데이터베이스가 분리되어 있어 요구사항 변경에 따라 특정 계층만 수정 가능합니다.
- 예를들어 UI가 React에서 Flutter로 변경되더라도 Controller와 Service Layer는 그대로 유지 가능합니다.
- 명확한 책임 분리 (Separation of Concerns)
- 각 계층이 특정한 역할과 책임을 담당하여 코드를 이해하기 쉽고, 협업이 쉽습니다.
'IT > 백엔드' 카테고리의 다른 글
클라이언트 - 서버간의 통신방식특징 (REST, GRPC, GRAPHQL) (0) | 2025.04.14 |
---|---|
[ node.js ] - 메모리 관리법 (0) | 2025.02.04 |
객체지향 프로그래밍의 개념 (0) | 2025.01.09 |
Garbage Collection 방식 (0) | 2024.12.24 |
[JAVA] - 메모리 관리법 (2) | 2024.12.11 |