핵심 OOP 원칙
SRP (단일 책임 원칙). 각 계층은 오직 자신의 책임에만 집중합니다.
계층형 아키텍처: 왜 역할을 나눠야 할까?
애플리케이션의 기능을 구현할 때, 각 코드 조각에 명확한 역할을 부여하면 훨씬 더 관리하기 좋은 구조를 만들 수 있습니다.
우리는 코드를 세 개의 계층으로 분리하여 각자의 책임에만 집중하도록 할 것입니다.
- Controller (프레젠테이션 계층): "외부의 요청을 받고, 응답을 보내는" 최전방 창구 역할.
- Service (비즈니스 계층): "핵심 비즈니스 로직을 처리하는" 실무 전문가 역할.
- Repository (데이터 접근 계층): "데이터베이스와 통신하는" 창고 관리자 역할.
- 공통 DTO 설계API를 구현하기 전에, 각 계층이 주고받을 데이터의 형식을 먼저 정의합니다.
- 이를 DTO(Data Transfer Object)라고 합니다.
package com.puzzlix.solid_task._global.dto;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 모든 API 응답을 감싸는 공통 DTO 설계
* @param <T>
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class CommonResponseDto<T> {
private boolean success;
private T data;
private String message;
// private CommonResponseDto(boolean success, T data, String message) {
// this.success = success;
// this.data = data;
// this.message = message;
// }
// 대체
// @AllArgsConstructor(access = AccessLevel.PRIVATE)
// 정적 팩토리 메서드 (팩토리 패턴과 다름 개념)
// static 객체 속성이 아니라 클래스에 포함 - ClassName.add();
public static <T> CommonResponseDto<T> success(T data, String message) {
return new CommonResponseDto<>(true, data, message);
}
public static <T> CommonResponseDto<T> success(T data) {
return success(data,null);
}
// 실패 응답을 생성하는 정적 팩토리 메서드
public static <T> CommonResponseDto<T> error(String message) {
return new CommonResponseDto<>(false, null, message);
}
/**
* 클라이언트 코드(Controller)로 부터 객체 생성 과정을 완전히 분리하고 숨기는 것이 목표다.
* 이는 주로 OCP(개방-폐쇄 원칙)을 만족시키는 코드이다.
*/
// new CommonResponseDto(); <- private 생성자
// CommonResponseDto.error("잘못된 비번입니다");
}
- IssueRequest
package com.puzzlix.solid_task.domain.issue.dto;
import lombok.Getter;
import lombok.Setter;
public class IssueRequest {
// 모바일에서 또는 클라언트에서 - 이슈 생성 요청
@Getter
@Setter
public static class Create {
// 클라이언트가 직접 입력해야 하는 정보 또는 셋팅 되어야 하는 정보
private String title;
private String description;
private Long projectId;
private Long reporterId;
} // end of static inner class
// IssueRequest.Create dto = new IssueRequest.Create(...);
}
package com.puzzlix.solid_task.domain.issue.dto;
import com.puzzlix.solid_task.domain.issue.Issue;
import com.puzzlix.solid_task.domain.issue.IssueStatus;
import java.util.ArrayList;
import java.util.List;
public class IssueResponse {
public static class FindAll {
private final Long id;
private final String title;
private final IssueStatus status;
// 생성자를 private 선언
private FindAll(Issue issue) {
this.id = issue.getId();
this.title = issue.getTittle();
this.status = issue.getIssueStatus();
}
// 정적 팩토리 메서드 선언 (이녀석은 제네릭이 아님)
// IssueResponse.from([issue, issue, issue]);
// Entity 를 DTO 로 변환하는 정적 팩토리 메서드를 만듬.
public static List<FindAll> from(List<Issue> issues) {
List<FindAll> dtoList = new ArrayList<>();
for (Issue issue : issues) {
dtoList.add(new FindAll(issue));
}
return dtoList;
}
}
}
package com.puzzlix.solid_task.domain.issue;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Repository // 이 클래스가 데이터 저장소 역할을 하는 스프링 빈임을 선언 함
public class MemoryIssueRepository implements IssueRepository {
// 동시성 문제를 방지하기 위해 ConcurrentHashMap 사용
private static Map<Long, Issue> store = new ConcurrentHashMap<>();
private static AtomicLong sequence = new AtomicLong(0);
@Override
public Issue save(Issue issue) {
// save 요청시 Issue 에 상태값 id가 없는 상태이다.
if(issue.getId() == null) {
// -> 1 변경 하고 issue 객체에 상태값 id를 1로 할당
issue.setId(sequence.incrementAndGet());
// sequence -> 1 로 결정됨
// sequence -> 2 로 결정됨
}
store.put(issue.getId(), issue);
return issue;
}
@Override
public Optional<Issue> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Issue> findAll() {
return new ArrayList<>(store.values());
}
}
서비스단 생성
package com.puzzlix.solid_task.domain.issue;
import com.puzzlix.solid_task.domain.issue.dto.IssueRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service // IoC
@RequiredArgsConstructor
public class IssueService {
// 구체 클래스가 아닌, IssueRepository 라는 역할(인터페이스)에만 의존한다.
private final IssueRepository issueRepository;
// DI 처리 함
// public IssueService(IssueRepository issueRepository) {
// this.issueRepository = issueRepository;
// }
// 이슈 생성 로직
public Issue createIssue(IssueRequest.Create request) {
Issue newIssue = new Issue();
newIssue.setTittle(request.getTitle());
newIssue.setDescription(request.getDescription());
newIssue.setReporterId(request.getReporterId());
// 이슈 --> TODO
newIssue.setIssueStatus(IssueStatus.TODO);
return issueRepository.save(newIssue);
}
// 모든 이슈 조회
public List<Issue> findIssues() {
return issueRepository.findAll();
}
}
컨트롤러 생성
package com.puzzlix.solid_task.domain.issue;
import com.puzzlix.solid_task._global.dto.CommonResponseDto;
import com.puzzlix.solid_task.domain.issue.dto.IssueRequest;
import com.puzzlix.solid_task.domain.issue.dto.IssueResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/issues")
@RequiredArgsConstructor
public class IssueController {
private final IssueService issueService;
/**
* 이슈 생성 API
* POST /api/issues
*/
@PostMapping
public ResponseEntity<CommonResponseDto<Issue>> createIssue(@RequestBody IssueRequest.Create request) {
Issue createdIssue = issueService.createIssue(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(CommonResponseDto.success(createdIssue));
}
/**
* 이슈 목록 조회 API
* GET /api/issues
*/
@GetMapping
public ResponseEntity<CommonResponseDto<List<IssueResponse.FindAll>>> getIssues() {
// 서비스에서 조회 요청
List<Issue> issues = issueService.findIssues();
// 조회된 도메인 이슈 리스트를 DTO로 변환
List<IssueResponse.FindAll> responseDtos = IssueResponse.FindAll.from(issues);
return ResponseEntity.ok(CommonResponseDto.success(responseDtos));
}
}
Controller, Service, Repository라는 세 개의 계층으로 역할을 완벽하게 분리하여 SRP를 준수하는 코드를 작성했습니다.
다음 단계 에서는 이 유연한 구조가 얼마나 강력한지, 메모리 저장소를 실제 데이터베이스로 바꾸는 과정을 통해서
장점들을 확인해 보자.
'JAVA' 카테고리의 다른 글
| [4단계] 도메인 연관관계 매핑 (JPA) (0) | 2025.11.14 |
|---|---|
| [3단계] 데이터베이스 연동과 JPA (DIP) (0) | 2025.11.14 |
| 컴포넌트 스캔 (0) | 2025.11.14 |
| 리플렉션(+어노테이션) (0) | 2025.11.14 |
| 우당탕탕 소셜로그인 구현(for kakao) -- 진행중 (0) | 2025.11.11 |