
핵심 개념: 관심사의 분리 (Separation of Concerns)
Layered Architecture의 핵심 목적은 관심사의 분리입니다.
그렇다면 왜 관심사를 분리해야 할까요?
가장 큰 이유는 유지보수성과 확장성 때문입니다.
사용자의 요청이 들어왔을 때, 각 Layer가 명확한 책임을 갖고 동작한다면 코드 수정이나 기능 추가가 훨씬 쉬워집니다.
Spring Boot로 개발할 때 우리는 자연스럽게 관심사를 다음과 같이 나누게 됩니다:
Controller (Presentation Layer): 사용자의 요청을 받고 응답을 반환
Service (Business Layer): 비즈니스 로직 처리
Repository (Persistence Layer): 데이터베이스 접근 및 처리
아래 예시를 보면서 각 Layer의 역할을 살펴보겠습니다.
사용자의 요청을 받고 요청값을 검증 후 반환합니다.
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/login")
public LoginResponse login(@RequestBody @Valid LoginRequest loginRequest) {
return service.login(loginRequest);
}
}
@Vali를 통해 LoginRequest을 검증합니다.
비즈니스 로직을 수행하는 핵심 구간입니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRespository userRepository;
public LoginResponse login(LoginRequest loginRequest) {
Users user = userRepository.findByEmail(loginRequest.email())
.orElseThrow(
LoginErrorCode.EMAIL_NOT_FOUND::exception
);
if(user.matchPassword(bCryptPasswordEncoder, loginRequest.password())) {
throw LoginErrorCode.PASSWORD_NOT_FOUND.exception();
}
return LoginResponse.builder()
.username(user.getUsername)
.build();
}
데이터베이스 접근을 담당합니다.
public interface UserRepository extends JpaRepository<Users,Long> {
}

이렇게 각 레이어가 자신의 역할에 집중하면:
저는 주로 서비스단을 테스트 코드로 작성해서 검증합니다.
서비스쪽이 비즈니스 로직을 담당하고 있는 부분이라 생각하여 제일 중요하다 생각합니다. 그래서 이번에 채팅방 생성하는 테스트 코드를 저의 방식대로 해봤습니다.
@ActiveProfiles("test")
@SpringBootTest
class ChatRoomTest {
@Autowired
private ChatRoomRepository chatRoomRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private BoardRepository boardRepository;
@Autowired
private ChatService chatService;
@DisplayName("방 생성 테스트")
@Test
void createRoom() {
// given
Users user = userRepository.findById(1L).orElseThrow();
Product product = boardRepository.findById(1L).orElseThrow();
Users user2 = userRepository.findById(2L).orElseThrow();
Product product2 = boardRepository.findById(2L).orElseThrow();
ChatRoomRequest request = ChatRoomRequest.builder()
.userId(user)
.productId(product)
.name("테스트 채팅방")
.build();
ChatRoomRequest request2 = ChatRoomRequest.builder()
.userId(user2)
.productId(product2)
.name("테스트 채팅방2")
.build();
List<ChatRoomEntity> chatRoom = ChatRoom.create(List.of(request, request2));
List<ChatRoomEntity> save = chatRoomRepository.saveAll(chatRoom);
// when
var chatRooms = chatRoomRepository.findAll();
// then
assertThat(chatRooms).hasSize(2);
assertThat(chatRooms.get(0).getProductId().getId()).isEqualTo(1L);
assertThat(chatRooms.get(0).getProductId().getId()).isEqualTo(2L);
}
}
application-test.yml을 만들어 테스트 환경을 구성하였으며 그 구성 기반으로
-- Location 테이블 생성
CREATE TABLE Location (
id SERIAL PRIMARY KEY,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
region_name VARCHAR(100),
full_address TEXT
);
-- Category 테이블 생성
CREATE TABLE Category (
id SERIAL PRIMARY KEY,
category_name VARCHAR(50)
);
-- Product 테이블 생성
CREATE TABLE Product (
id SERIAL PRIMARY KEY,
title VARCHAR(100),
description TEXT,
price INT,
status VARCHAR(20),
views INT,
user_id INT,
location_id INT,
category_id INT,
FOREIGN KEY (location_id) REFERENCES Location(id),
FOREIGN KEY (category_id) REFERENCES Category(id)
);
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE chat_rooms (
room_id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES product(id)
);
-- 예시 데이터 삽입
INSERT INTO users (username, email, password) VALUES
('user1', 'user1@example.com', 'password123'),
('user2', 'user2@example.com', 'password456'),
('user3', 'user3@example.com', 'password789');
-- Location 데이터 삽입
INSERT INTO Location (latitude, longitude, region_name, full_address) VALUES
(37.5665, 126.9780, '서울시 종로구', '서울특별시 종로구 세종대로 1'),
(37.5079, 127.0600, '서울시 강남구', '서울특별시 강남구 테헤란로 425'),
(35.1796, 129.0756, '부산시 해운대구', '부산광역시 해운대구 해운대해변로 1'),
(35.6895, 139.6917, '도쿄', '일본 도쿄도 지요다구'),
(37.7749, -122.4194, '샌프란시스코', '미국 캘리포니아주 샌프란시스코');
-- Category 데이터 삽입
INSERT INTO Category (category_name) VALUES
('전자기기'),
('의류'),
('가구'),
('스포츠'),
('도서'),
('음악'),
('악세서리'),
('주방용품'),
('화장품'),
('애완동물');
-- Product 데이터 삽입
INSERT INTO Product (title, description, price, status, views, user_id, location_id, category_id) VALUES
('아이폰 13', '새 제품, 미개봉', 1000000, 'NEW', 0, 1, 1, 1),
('삼성 TV', '사용감 있는 TV, 정상 작동', 500000, 'USED', 0, 2, 1, 3),
('운동화', '새 상품, 사이즈 270', 80000, 'NEW', 0, 1, 2, 2),
('소파', '3인용 소파, 약간의 생활 스크래치 있음', 150000, 'USED', 0, 1, 3, 3),
('노트북', '맥북 에어 2020, 상태 매우 좋음', 1200000, 'NEW', 0, 1, 1, 1),
('자전거', '새 자전거, 26인치', 300000, 'NEW', 0, 1, 2, 4),
('책상', '원목 책상, 약간의 사용 흔적 있음', 120000, 'USED', 0, 1, 3, 3),
('디지털 카메라', '소니 카메라, 새 제품', 500000, 'NEW', 0, 1, 2, 1),
('화장품 세트', '다양한 화장품 포함 세트', 70000, 'NEW', 0, 1, 1, 9),
('강아지 사료', '건강한 강아지 사료 1kg', 15000, 'NEW', 0, 1, 4, 10);
더미 데이터를 만들어서 테스트를 진행했습니다.