메모장 CRUD 개선 - 3 Layered Architecture

하마·2025년 3월 22일

Spring

목록 보기
18/22
post-thumbnail

1. 요구사항 분석 & 설계


1. 컨트롤러의 책임 분리


  • 현재 컨트롤러가 모든 것을 담당하고 있음
    • 요청, 응답, 비즈니스 로직, 예외 처리 등
  • 3 Layered Architecture 개념을 적용하여 코드를 리팩토링함
    • Controller , Service , Repository 레이어로 분리하여 책임을 나눔

2. API 구현


1. Controller 책임 분리


1. Service 레이어 분리

1. MemoService

public interface MemoService {

}

2. MemoServiceImpl

// 실제 비즈니스 로직 관련 처리는 서비스 레이어의 구현체에서 진행
@Service
public class MemoServiceImpl implements MemoService {

}

@Service = @Component 즉, Spring Bean으로 등록됨
명시적으로 Service 레이어를 나타내는 역할

2. Repository 레이어 분리

1. MemoRepository

public interface MemoRepository {

}

2. MemoRepositoryImpl

// 실제 DB와 상호작용하여 데이터를 CRUD하는 작업 수행
@Repository
public class MemoRepositoryImpl implements MemoRepository {
	private final Map<Long, Memo> memoList = new HashMap<>();
}

@Repository = @Component 즉, Spring Bean으로 등록됨
명시적으로 Repository 레이어를 나타는 역할


2. 메모 생성 API 리팩토링


1. Controller

MemoController

@RestController
@RequestMapping("/memos")
public class MemoController {

    // 주입된 의존성을 변경할 수 없어 객체의 상태를 안전하게 유지할 수 있다.
	private final MemoService memoService;

    /**
     * 생성자 주입
     * 클래스가 필요로 하는 의존성을 생성자를 통해 전달하는 방식
     * @param memoService @Service로 등록된 MemoService 구현체인 Impl
     */
    public MemoController(MemoService memoService) {
		this.memoService = memoService;
    }

    @PostMapping // 요청
    public ResponseEntity<MemoResponseDto> createMemo(@RequestBody MemoRequestDto requestDto) {
		// ServiceLayer 호출 및 응답
        return new ResponseEntity<>(memoService.saveMemo(requestDto), HttpStatus.CREATED);
    }
}

@RestController = @Controller + @ResponseBody
@RequestMapping : url의 prefix 설정

2. Memo

Memo

@Getter
@AllArgsConstructor
public class Memo {

    @Setter
    private Long id;
    private String title;
    private String contents;

    public Memo(String title, String contents) {
        this.title = title;
        this.contents = contents;
    }

	// 기존 사용하던 dto 대신 기본 자료형으로 매개변수 사용
    // 메소드의 유연성과 재사용성이 높아짐
    // 테스트 코드에도 도움 됨
    public void update(String title, String contents) {
        this.title = title;
        this.contents = contents;
    }

    public void updateTitle(String title) {
        this.title = title;
    }

}

클래스 레벨에서 @Setter 사용 시 전체 필드에 Setter 적용
필드 레벨에서 사용해야 원하는 필드에 @Setter 적용할 수 있음

3. Service Layer

1. MemoService

public interface MemoService {
	MemoResponseDto saveMemo(MemoRequestDto requestDto);
}

2. MemoServiceImpl

@Service
public class MemoServiceImpl implements MemoService {

    private final MemoRepository memoRepository;

    public MemoServiceImpl(MemoRepository memoRepository) {
        this.memoRepository = memoRepository;
    }

	// 생성자 주입
    @Override
    public MemoResponseDto saveMemo(MemoRequestDto requestDto) {

        // 요청받은 데이터로 Memo 객체 생성
        // ID 없음 - Repository 영역이기 때문
        Memo memo = new Memo(requestDto.getTitle(), requestDto.getContents());

        // Inmemory DB에 Memo 저장
        Memo savedMemo = memoRepository.saveMemo(memo);

        return new MemoResponseDto(savedMemo);
    }
}

4. Repository Layer

1. MemoRepository

public interface MemoRepository {
	Memo saveMemo(Memo memo);
}

2. MemoRepositoryImpl

@Repository
public class MemoRepositoryImpl implements MemoRepository {

	// 기존 MemoController에 있던 자료구조를 여기서 사용
    private final Map<Long, Memo> memoList = new HashMap<>();

    @Override
    public Memo saveMemo(Memo memo) {

        // memo 식별자 자동 생성
        Long memoId = memoList.isEmpty() ? 1 : Collections.max(memoList.keySet()) + 1;
        memo.setId(memoId);

        memoList.put(memoId, memo);

        return memo;
    }
}

3. 메모 전체 조회 API 리팩토링


1. Controller

MemoController

@RestController
@RequestMapping("/memos") // Prefix
public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }

    @GetMapping
    public List<MemoResponseDto> findAllMemos() {
        return memoService.findAllMemos();
    }
    
}

2. Service

1. MemoService

public interface MemoService {
	List<MemoResponseDto> findAllMemos();	
}

2. MemoServiceImple

@Service
public class MemoServiceImpl implements MemoService {
	
	private final MemoRepository memoRepository;
	
	public MemoServiceImpl(MemoRepository memoRepository) {
	      this.memoRepository = memoRepository;
	}
		
    @Override
    public List<MemoResponseDto> findAllMemos() {
        return memoRepository.findAllMemos();
    }

}

3. Repository

1. MemoRepository

public interface MemoRepository {
	List<MemoResponseDto> findAllMemos();
}

2. MemoRepositoryImpl

@Repository
public class MemoRepositoryImpl implements MemoRepository {

    private final Map<Long, Memo> memoList = new HashMap<>();

    @Override
    public List<MemoResponseDto> findAllMemos() {
        List<MemoResponseDto> allMemos = new ArrayList<>();
		
        // DB에 있는 데이터를 하나씩 꺼내와서 allMemos 리스트에 저장
        // HashMap<Memo> -> List<MemoResponseDto>
        for (Memo memo : memoList.values()) {
            MemoResponseDto responseDto = new MemoResponseDto(memo);
            allMemos.add(responseDto);
        }

        return allMemos;
    }
}

4. 메모 선택 조회 API 리팩토링


1. Controller

MemoController

@RestController 
@RequestMapping("/memos") 
public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<MemoResponseDto> findMemoById(@PathVariable Long id) {
        return new ResponseEntity<>(memoService.findMemoById(id), HttpStatus.OK);
    }
   
}

2. Service

1. MemoService

public interface MemoService {
	MemoResponseDto findMemoById(Long id);
}

2. MemoServiceImpl

@Service
public class MemoServiceImpl implements MemoService {
	
	private final MemoRepository memoRepository;
	
	public MemoServiceImpl(MemoRepository memoRepository) {
		this.memoRepository = memoRepository;
	}
		
    @Override
	public MemoResponseDto findMemoById(Long id) {
		Memo memo = memoRepository.findMemoById(id);

        // ResponseEntity<HttpStatus.NOT_FOUND> 사용 불가
		if (memo == null) {
			throw new ResponseStatusException(HttpStatus.NOT_FOUND,
            	"dose not exist id: " + id);
		}

        return new MemoResponseDto(memo);
    }

}

3. Repository

1. MemoRepository

public interface MemoRepository {
	Memo findMemoById(Long id);
}

2. MemoRepositoryImpl

@Repository
public class MemoRepositoryImpl implements MemoRepository {

    private final Map<Long, Memo> memoList = new HashMap<>();

    @Override
    public Memo findMemoById(Long id) {
        return memoList.get(id);
    }
    
}

5. 메모 전체 수정 API 리팩토링


1. Controller

MemoController

@RestController
@RequestMapping("/memos")
public class MemoController {

    private final MemoService memoService;
    
    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }

    @PutMapping("/{id}")
    public ResponseEntity<MemoResponseDto> updateMemo(
            @PathVariable Long id,
            @RequestBody MemoRequestDto requestDto
    ) {
        return new ResponseEntity<>(memoService.updateMemo(id, requestDto.getTitle(), requestDto.getContents()), HttpStatus.OK);
    }
    
}

2. Service

1. MemoService

public interface MemoService {
	MemoResponseDto updateMemo(Long id, String title, String contents);
}

2. MemoServiceImpl

@Service
public class MemoServiceImpl implements MemoService {
	
	private final MemoRepository memoRepository;
	
	public MemoServiceImpl(MemoRepository memoRepository) {
		this.memoRepository = memoRepository;
	  }
		
    @Override
    public MemoResponseDto updateMemo(Long id, String title, String contents) {
        // memo 조회
        Memo memo = memoRepository.findMemoById(id);

        // NPE 방지
        if (memo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
        }

        // 필수값 검증
        if (title == null || contents == null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title and content are required values.");
        }

        // memo 수정
        memo.update(title, contents);

        return new MemoResponseDto(memo);
    }

}

3. Repository

메모리에 존재하는 Memo를 직접 수정함
DB 접근이 필요없음


6. 메모 제목 수정 API 리팩토링


1. Controller

MemoController

@RestController
@RequestMapping("/memos")
public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }

    @PatchMapping("/{id}")
    public ResponseEntity<MemoResponseDto> updateTitle(
            @PathVariable Long id,
            @RequestBody MemoRequestDto requestDto
    ) {
        return new ResponseEntity<>(memoService.updateTitle(id, requestDto.getTitle(), requestDto.getContents()),
        	HttpStatus.OK);
    }
    
}

2. Service

1. MemoService

public interface MemoService {
	MemoResponseDto updateTitle(Long id, String title, String contents);
}

2. MemoServiceImpl

@Service
public class MemoServiceImpl implements MemoService {
	
	private final MemoRepository memoRepository;
	
	public MemoServiceImpl(MemoRepository memoRepository) {
		this.memoRepository = memoRepository;
	}
		
    @Override
    public MemoResponseDto updateTitle(Long id, String title, String contents) {
        // memo 조회
        Memo memo = memoRepository.findMemoById(id);

        // NPE 방지
        if (memo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Does not exist id = " + id);
        }
        // 필수값 검증
        if (title == null || contents != null) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The title is a required value.");
        }

        memo.updateTitle(title);

        return new MemoResponseDto(memo);
    }

}

3. Repository

메모리에 존재하는 Memo를 직접 수정함
DB 접근이 필요없음


7. 메모 삭제 API 리팩토링

1. Controller

MemoController

@RestController
@RequestMapping("/memos")
public class MemoController {

    private final MemoService memoService;

    public MemoController(MemoService memoService) {
        this.memoService = memoService;
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteMemo(@PathVariable Long id) {

        memoService.deleteMemo(id);
        // 성공한 경우
        return new ResponseEntity<>(HttpStatus.OK);
    }
    
}

2. Service

1. MemoService

public interface MemoService {
	void deleteMemo(Long id);
}

2. MemoServiceImpl

@Service
public class MemoServiceImpl implements MemoService {
	
	private final MemoRepository memoRepository;
	
	public MemoServiceImpl(MemoRepository memoRepository) {
		this.memoRepository = memoRepository;
	}
		
    @Override
    public void deleteMemo(Long id) {
        // memo 조회
        Memo memo = memoRepository.findMemoById(id);

        // NPE 방지
        if (memo == null) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND,
            	"does not exist id = " + id);
        }

        memoRepository.deleteMemo(id);
    }

}

3. Repository

1. MemoRepository

public interface MemoRepository {	
	void deleteMemo(Long id);
}

2. MemoRepositoryImpl

@Repository
public class MemoRepositoryImpl implements MemoRepository {

    private final Map<Long, Memo> memoList = new HashMap<>();

    @Override
    public void deleteMemo(Long id) {
        memoList.remove(id);
    }
    
}

8. GitHub



3. 해결한 문제점


  • Controller의 책임을 레이어 별로 분리했다.

4. 해결하지 못한 문제점


  • 서버가 종료된 후 다시 켜지면 데이터가 모두 초기화 된다.
  • 예외의 공통 처리가 불가능하다.
    • 각각의 모든 예외를 try-catch 해야 함
  • RequestDto , ResponseDto 를 공유하여 null 이 들어오기도 한다.

참고자료


스프링 입문 - 6주차

  • 3 Layered Architecture 실습

0개의 댓글