[초록스터디] Spring core

hee·2025년 6월 25일

Spring

목록 보기
2/4

시작에 앞서
Spring을 초록스터디를 통해 처음 공부해보았는데, 첫번째 방탈출 미션을 드디어 완료하였다. JAVA 미션을 진행할때는 잘 몰랐던 것을 배울 수 있어서 좋았다. 특히, 멘토 오찌님이 공부하고 생각할 거리를 많이 주셔서 배울 수 있었던 것 같다. 이번 core 미션은 Spring context를 이해하는데에 초점을 맞추어 공부했으나, 어렵다 . . . 따라서, 이번 미션을 진행하며 고민해보았거나, 오찌님이 질문해주신 부분들을 정리해두려 한다.

1. Spring Context란?

Spring Context 란 빈(Bean) 객체들을 생성하고 관리하는 스프링의 핵심 컨테이너이다.

  • Bean: 스프링이 관리하는 객체 (Controller, Service, Repository)
  • Container: Bean의 생명주기, 의존성 주입 등을 관리하는 스프링 내부 시스템
  • Context: Bean 등록, 조회, 설정 등을 처리하는 객체 = ApplicationContext

2. 예외처리 위치

예외처리는 도메인에서 처리하는 것이 좋을까, 서비스에서 처리하는 것이 좋을까

도메인에서 처리할 수 있는 예외는 도메인에서, 그 외에는 서비스에서 처리하는 것이 좋다.
-> 최대한 더 낮은 계층에서 처리하는 것을 원칙으로 삼자.

시간 객체의 시간 값에 null이 들어왔다 -> 도메인에서 예외처리
시간 객체의 시간 값은 중복이면 안된다 -> 도메인 객체만으로는 안되고 저장된 다른 객체를 봐야함 -> 서비스에서 예외처리

3. Controller -> Repository 직접 의존

[Controller]
↓ 요청 전달 / 응답 반환 [DTO][Service]
↓ 비즈니스 로직 처리
[Repository]
↓ DB 접근
[Domain]
↔ DB 매핑

+) [Exception] : 예외처리
+) [View] : 화면에 보이는 것

이와 같은 관계를 갖고 애플리케이션은 설계되는데,
Controller 는 요청/응답 처리에 집중해야하며, Repository는 DB 접근에 집중해야함.
또한, 비즈니스 로직은 Service에서 진행해야함.

-> 이 부분 제발 좀 ... 지키자 희정아 ...
✅ 설계 시작 전에 계층별 책임 적어보기 "이건 Service 책임인가?" 먼저 생각하기
✅ 단위 테스트 작성 비즈니스 로직은 Service에서 테스트할 수 있어야 함
✅ 객체 간 의존 구조 그려보기 간단한 UML, 화살표 그리기 등으로 시각화
✅ Controller에서 로직이 길어지면 "서비스로 옮길 수 있지 않을까?" 되묻기

4. @Autowired 없이도 작동 ?

public class ReservationController {
    private final ReservationService reservationService;

    @Autowired
    public ReservationController(ReservationService reservationService) {
        this.reservationService = reservationService;
    } 

-> 이 코드에서는 @Autowired 없이도 작동 가능함.
Spring 4.3부터 클래스에 생성자가 하나 뿐이라면, 자동으로 주입가능한 Bean을 찾아서 주입함.
당연히, 생성자 2개 이상일 때는 생략하지 않아도 됨.

5. 상태코드 메서드

ResponseEntity
.status(HttpStatus.CREATED)
.location(location)
.body(ReservationResponse.from(savedReservation));

ResponseEntity
.created(location)
.body(ReservationResponse.from(savedReservation));

로 축약할 수 있음.

+)
ok = 200
created(URI) = 201
accepted() = 202
noContent() = 204
badRequest() = 400
notFound() = 404

6. 삭제를 의도했는데 삭제가 되지 않은 상황의 예외처리는 서비스에서 !

  • TimeController
boolean deleted = timeService.deleteTime(id);
        if (!deleted) {
            return ResponseEntity.badRequest().build();
        }

본래 Controller에서 단순 삭제 성공 여부만 판단하고 있다고 생각하여 Controller에 예외처리 로직을 작성하였는데, 삭제 실패와 같은 비즈니스 로직 실패는 Service에서 다루는 것이 맞는 방식이다.

따라서, 말씀해주신대로,
삭제 실패 -> Service
성공 흐름 -> Controller
나머지 예외는 exception에서 처리하는 것으로 수정하였다.

<수정 후>

  1. Controller는 성공한 흐름만 다룸
@DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteTime(@PathVariable Long id) {
        timeService.deleteTime(id);
        return ResponseEntity.noContent().build();
    }

2.Service에서 예외 표현함(비즈니스 로직 예외)

public boolean deleteTime(Long id) {
        if(!timeRepository.deleteById(id)) {
            throw new InvalidTimeException("없는 시간입니다." + id);
        }
        return timeRepository.deleteById(id);
    }

7. 도메인에 setter

도메인 객체에 setter를 열어두는 것은 객체 상태를 마음대로 바꿀 수 있어, 캡슐화에 어긋나고, 객체 불변성 유지에 어려워짐.

보통 도메인 객체를 수정해야하는 경우
서비스 클래스에서 도메인 객체를 받아
서비스 클래스에서 상태 변경을 수행하고,
변경된 도메인 객체를 Repository에 저장하는 방식을 사용할 것 !

8. validation 꼭. 유효성 검증 꼬옥 !!!

까먹지 말긔 ...

9. LocalTime vs String

  1. LocalTime
  • 타입 안정성: 시간 정보를 명확한 타입으로 표현해서, 시간 연산(더하기, 빼기 등)을 안전하게 수행 가능
  • 유효성 검사 내장: 잘못된 시간 값(예: 25:00)은 생성 자체가 안 됨
  • 표준 API 지원: 포맷팅, 파싱, 비교, 계산 등 편리한 메서드 제공
  • 명확한 의미 전달: 변수 타입만 보고도 시간 정보임을 알 수 있음
  • DB 매핑 주의 필요: JDBC에서 LocalTime 직접 매핑이 안 될 수 있어, java.sql.Time으로 변환 필요
  • 직렬화/역직렬화 설정 필요: JSON 직렬화 시 기본 String 포맷 지정 필요(예: HH:mm:ss)
  1. String
  • 단순 저장/전송: 포맷을 문자열로 자유롭게 표현 가능 (예: "10:00", "10시", "10:00 AM")
  • 빠른 구현: 복잡한 타입 변환 없이 바로 사용 가능
  • 유효성 검사 어려움: "25:00", "abc" 같은 잘못된 시간 값도 들어갈 수 있음
  • 시간 연산 불가: 시간 더하기 빼기 등 로직 작성 시 문자열 파싱 필요
  • 타입 의미 모호: 그냥 문자열이어도 시간으로 처리 가능

0개의 댓글