
지난번에 여러 아키텍처 패턴들을 간단히 소개했었습니다.
이번에는 제가 실무에서 주로 사용하는 레이어드 아키텍처에 대해 깊이 있게 살펴보려고 합니다. 실무에서 많이 사용되는 패턴 중 하나이기 때문에 제대로 이해하는 것이 중요합니다.
이번에는 각 레이어의 역할과 그에 맞는 주요 코드 예시까지 함께 다뤄보도록 하겠습니다.
레이어드 아키텍처는 시스템을
프레젠테이션 레이어, 비즈니스 로직 레이어, 데이터 액세스 레이어, 인프라스트럭처 레이어로 나누어 구성합니다.
이를 통해 각 레이어가 명확한 책임을 가지며, 코드의 유지보수성과 가독성을 높입니다.
Layered Architecture를 사용하는 이유
Layered Architecture의 가장 큰 장점은 코드의 모듈화입니다. 각 계층이 독립적으로 작동할 수 있기 때문에 새로운 기능을 추가하거나 기존 기능을 수정할 때 전체 코드를 건드리지 않아도 됩니다. 또한, 계층 간의 의존성을 최소화함으로써 코드의 유지 보수성과 테스트 용이성을 높일 수 있습니다.
프레젠테이션 레이어는 사용자와 상호작용하는 부분입니다.
주로 컨트롤러 역할을 수행하며, 사용자의 요청을 받아 비즈니스 로직에 전달하고, 처리된 결과를 사용자에게 반환합니다.
// 코드 예시: UserController
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
try {
UserResponse userResponse = userService.getUserById(id);
return new ResponseEntity<>(userResponse, HttpStatus.OK);
} catch (UserNotFoundException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@PostMapping
public ResponseEntity<UserResponse> createUser(@RequestBody CreateUserRequest request) {
UserResponse userResponse = userService.createUser(request);
return new ResponseEntity<>(userResponse, HttpStatus.CREATED);
}
}
UserController는 사용자의 요청을 받아 적절한 서비스 메서드를 호출합니다.
getUserById메서드는 사용자 조회 요청을 처리하고, 결과를 클라이언트에게 반환합니다.
createUser는 사용자 생성 요청을 받아 처리 후 클라이언트에게 반환합니다.
비즈니스 로직 레이어는 애플리케이션의 핵심 규칙과 로직을 구현하는 곳입니다.
프레젠테이션 레이어로부터 전달된 요청을 처리하고, 데이터 액세스 레이어와 상호작용합니다.
// 코드 예시: UserService (Spring Boot 예시)
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public UserResponse getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found with id " + id));
if (!user.isActive()) {
throw new InactiveUserException("User is inactive");
}
return new UserResponse(user);
}
public UserResponse createUser(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException("Email already exists");
}
User user = new User(request.getName(), request.getEmail());
userRepository.save(user);
return new UserResponse(user);
}
}
getUserById는 주어진 ID의 사용자를 조회하고, 비즈니스 규칙에 따라 유효성을 검사합니다. 예를 들어, 사용자가 비활성 상태일 경우 예외를 발생시킵니다.
createUser는 이메일 중복을 확인하는 비즈니스 규칙을 적용한 후, 새로운 사용자를 생성합니다.
데이터 액세스 레이어는 데이터베이스와 상호작용하는 계층으로, CRUD 작업을 수행합니다.
// 코드 예시: UserRepository (Spring Data JPA 예시)
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 사용자 이메일로 존재 여부 확인
boolean existsByEmail(String email);
// 사용자 활성 상태 필터링
List<User> findByIsActiveTrue();
}
existsByEmail는 이메일 중복 여부를 확인하는 메서드입니다.
findByIsActiveTrue는 활성 상태인 사용자만 조회하는 메서드로, 비즈니스 로직에서 필요한 정보를 효율적으로 가져올 수 있게 합니다.
// 코드 예시: User 엔티티 (Spring Data JPA 예시)
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private boolean isActive;
// Constructor, Getters, and Setters omitted for brevity
public User(String name, String email) {
this.name = name;
this.email = email;
this.isActive = true;
}
public boolean isActive() {
return isActive;
}
public void deactivate() {
this.isActive = false;
}
}
User는 엔티티는 데이터베이스에 저장될 사용자 객체를 나타냅니다.
isActive는 필드를 통해 사용자의 활성 상태를 관리합니다.
인프라스트럭처 레이어는 외부 시스템과의 상호작용, 파일 처리, 메시지 큐와 같은 기능을 담당하는 계층입니다. 외부 API 호출이나 서드파티 라이브러리 사용 등이 포함됩니다.
// 코드 예시: 외부 API 호출 (RestTemplate 예시)
@Service
public class ExternalApiService {
private final RestTemplate restTemplate;
@Autowired
public ExternalApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public ExternalApiResponse callExternalApi(String endpoint) {
ResponseEntity<ExternalApiResponse> response = restTemplate.getForEntity(endpoint, ExternalApiResponse.class);
if (response.getStatusCode() == HttpStatus.OK) {
return response.getBody();
} else {
throw new ExternalApiException("Failed to fetch data from external API");
}
}
}
ExternalApiService는 외부 API를 호출하여 결과를 받아오는 역할을 합니다.
이 계층은 비즈니스 로직이 외부 API와 직접적으로 연동되지 않도록 하는 역할을 합니다.
레이어드 아키텍처의 가장 큰 장점은 유지보수성입니다.
각 레이어가 명확한 책임을 가지기 때문에, 특정 기능에 문제가 생겼을 때 어느 레이어에서 수정해야 하는지 쉽게 파악할 수 있습니다. 또한 코드의 구조가 명확하기 때문에 가독성이 높아집니다.
각 계층이 독립적이기 때문에, 특정 계층만 따로 테스트할 수 있습니다.
예를 들어, 비즈니스 로직을 테스트할 때 데이터베이스 연결 없이도 비즈니스 규칙을 검증할 수 있습니다.
이처럼 단위 테스트와 통합 테스트 모두에서 유리한 구조를 가집니다.
새로운 기능을 추가할 때도, 특정 레이어에만 집중적으로 변경을 하면 되기 때문에 코드 변경이 비교적 용이합니다.
이로 인해 확장성이 확보됩니다.
레이어드 아키텍처는 각 계층 간의 호출을 통해 동작하는 구조입니다.
따라서 계층 간의 호출이 빈번해질 경우 성능 저하가 발생할 수 있습니다.
특히 대규모 시스템에서 여러 계층을 거쳐야 할 때 이 문제가 두드러집니다.
애플리케이션이 커질수록 레이어 간 의존성이 증가하고, 복잡성이 크게 늘어날 수 있습니다.
이로 인해 특정 기능이 여러 레이어에 걸쳐 있어야 하거나, 각 레이어의 경계가 모호해질 경우 유지보수가 오히려 어려워질 수 있습니다.
대규모 전자상거래 애플리케이션에서 사용자는 상품을 검색하고 장바구니에 담습니다.
프레젠테이션 레이어는 검색 결과를 사용자에게 표시하고, 비즈니스 로직 레이어는 상품의 재고 상태를 관리하며, 데이터 액세스 레이어는 상품 정보를 데이터베이스에서 가져옵니다.
이처럼 각 레이어가 독립적으로 동작하므로 시스템이 확장될 때 유연하게 대응할 수 있습니다.
블로그에서 사용자는 글을 작성하고 수정합니다.
프레젠테이션 레이어는 사용자가 글을 작성할 수 있는 UI를 제공하고, 비즈니스 로직 레이어는 작성된 글이 규칙에 맞는지 검증하며, 데이터 액세스 레이어는 해당 글을 데이터베이스에 저장합니다.
이렇게 레이어드 아키텍처를 통해 블로그 기능을 확장해나갈 수 있습니다.
레이어드 아키텍처는 그 단순함과 명확한 구조로 많은 개발자가 선호하는 아키텍처 패턴입니다. 하지만 각 레이어 간의 복잡한 상호작용을 관리하는 것이 중요하며, 성능을 고려한 설계가 필요합니다.
이번 글에서 다룬 코드 예시들이 실무에서의 레이어드 아키텍처 이해에 도움이 되었기를 바랍니다.
다음 포스팅에서는 레이어드 아키텍처의 확장성과 성능을 개선할 수 있는 팁을 다뤄보겠습니다!