계층 분리란?

계층 분리란 애플리케이션의 책임과 역할을 명확히 구분하여 각 부분이 독립적으로 동작하도록 설계하는 방식입니다.

계층의 일반적인 구성

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)
    • 각 계층이 특정한 역할과 책임을 담당하여 코드를 이해하기 쉽고, 협업이 쉽습니다.

+ Recent posts