구현순서 (Bottom-Up: 상향식. 즉, 아래(도메인)부터 위(컨트롤러)로 )
도메인 엔티티 → DTO & Command (데이터 구조 정의) → Repository Interface → Service (비즈니스 로직) → 컨트롤러
<aside> 💡
구분 기준 (위 경우는 실행 흐름 관점)
실행 흐름 관점의 '상/하위’ → 데이터의 흐름이나 호출 순서를 기준
(사용자와 가까운 컨트롤러가 상위 / db와 가까운 도메인이 하위)
의존성 규칙 관점의 '고/저수준’ → 누가 더 중요하고, 누가 누구에게 의존해야 하는가의 기준
(비즈니스 로직을 담은 도메인이 고수준 / 서비스를 의존하는 컨트롤러가 저수준)
</aside>
@NotNull vs @Column의 nullable=false
엔티티 설계 시, 일반적으로 @Column 어노테이션을 활용
→ @Column의 nullable=false 설정으로 NOT NULL 제약조건 명시 가능 (기본값은 true)
→ 이 경우, 데이터베이스 컬럼의 속성이기에 애플리케이션 내부에서는 NULL 값이 허용됨. (데이터베이스 쿼리가 실행될 때 예외 발생)
@NOT NULL 어노테이션도 마찬가지로 DDL 상에서 해당 컬럼이 NOT NULL 제약조건을 가짐을 명시 가능
→ 차이점은 스프링(자바) 애플리케이션 내에서 예외를 잡아줌. (불필요한 쿼리 발생 X) (javax.validation.ConstraintViolationException 예외 발생)
→ 엔티티가 영속화되는 시점에 예외 발생
StringUtils.hasText: Spring 프레임워크가 제공하는 유용한 문자열 검증 유틸리티 메서드
→ 3가지 모두 검사 (성공 시, true 반환)
null
이 아닌가?""
)이 아닌가?" "
)이 아닌가?엔티티의 데이터 삭제 (연관된 엔티티)
//3가지 케이스
this.activeDays.clear(); //연결관계 끊기 (리스트이기에 clear로 전체 삭제)
this.partyImg = null; //연결관계 끊기 (단일 객체기에 null)
this.content = request.content(); //연관된 엔티티가 아닌 그냥 필드인 경우, 값만 넣어줘도 됨
양방향 관계 설정 (연관관계 편의 메서드)
public void addActiveDay(ActiveDay day) {
//자식(PartyActiveDay)을 부모(Party) 안에서 직접 생성
PartyActiveDay partyActiveDay = PartyActiveDay.builder()
.activeDay(day)
.party(this) //**생성 시점에 바로 부모-자식 관계 설정**
.build();
this.activeDays.add(partyActiveDay); //부모에게 자식을 설정
}
public void addMember(MemberParty memberParty) {
this.memberParties.add(memberParty); //부모에게 자식을 설정
memberParty.setParty(this); //이미 만들어진 자식에게 부모를 설정
}
@NotNull vs @NotEmpty vs @NotBlank
record형식으로 dto를 만들면 Getter
, equals()
, hashCode()
, toString()
등이 자동으로 구현
→ 단순히 데이터를 담는 목적의 불변 객체는 record로 구현하면 좋음.
하나의 기능과 관련된 모든 DTO를 하나의 클래스 안에 중첩된 record로 묶어서 관리하는 패턴으로도 구현 가능
dto계층에서의 command객체는 POST나 PATCH처럼 복잡한 요청 본문(body)을 가진 데이터를 서비스 계층에 전달하기 위해 사용. 일반적으로 클라이언트에서 보내는 요청은 RequestDTO 객체로 처리해주면 됨.
builder 패턴 사용
// 생성된 Party 엔티티를 최종 응답 DTO로 변환
public PartyCreateResponseDTO toCreateResponseDTO(Party party) {
return PartyCreateResponseDTO.builder()
.partyId(party.getId())
.createdAt(party.getCreatedAt())
.build();
}
stream
.stream: List를 Stream으로 만드는 함수
.collect(Collectors.toMap(key, value)): 스트림의 각 항목을 모아서(collect) Map으로 만드는 함수 (최종연산)
//예시
Map<Long, Integer> exerciseCountMap = exerciseRepository.findTotalExerciseCountsByPartyIds(partyIds)
.stream()
.collect(Collectors.toMap(PartyExerciseInfoDTO::getPartyId, dto -> dto.getCount().intValue()));
toMap에 3개의 파라메터가 들어갈 경우 마지막 파라메터는 중복 key 처리 방법
.collect(Collectors.toMap(
exercise -> exercise.getParty().getId(), // key: partyId
this::formatNextExerciseInfo, // value: String (운동 정보)
(existing, replacement) -> existing // key 중복 시 이미 들어있는 값(existing) 유지하고 나머지 버림
// 즉, 최초로 등장한 값만 사용
));
.map: 컬렉션(리스트 등)의 각 요소를 다른 형태나 값으로 변환할 때 사용하는 함수
List<Long> partyIds = partySlice.getContent().stream() //Party 객체들을 컨베이어 벨트에 올림
.map(Party::getId) //각 Party 객체를 getId()로 가공해서 'ID(Long)'만 남김
.toList(); // -> [1L, 2L, 3L]의 Long타입 리스트로 바뀜.
List<ActivityTime> enums = activityTimes.stream()
.map(ActivityTime::valueOf) //각 activityTimes 객체가 ENUM으로 변환됨
.toList();
.concat: 두 개의 스트림을 하나로 이어 붙여 새로운 스트림으로 만드는 함수
// 두 리스트를 합쳐서 반환
return Stream.concat(existingMemberIds.stream(), pendingMemberIds.stream()).distinct().toList();
.forEach: 스트림을 순회하면서 각 요소에 동작을 수행하는 함수 (최종연산)
stream.forEach(element -> System.out.println(element));
.of: 스트림을 만드는 정적 팩토리 메서드
Stream<String> stream = Stream.of("A", "B", "C"); //"A", "B", "C"라는 세 요소를 가진 스트림 생성
삼항연산자
→ ( 조건 ) ? TRUE인 경우 : FALSE인 경우
// status가 PENDING이 아닐 경우에만 updatedAt 값을 설정
LocalDateTime updatedAt = (request.getStatus() != RequestStatus.PENDING) ? request.getUpdatedAt() : null;
enum 비교
→ 자바에서 Enum 타입은 싱글턴(Singleton)으로 관리되기에 단 하나만 존재함.
→ 즉, 메모리 주소를 비교하는 **==, !=**이 적합함. (.equals()보다 빠름)
invitation.getStatus() != RequestStatus.PENDING
데이터 변환
Parse: 텍스트를 분석하여 **구조화된 데이터(객체)**로 변환 → 파싱
ex) String
→ Object
Convert: 한 데이터 형식을 다른 데이터 형식으로 변환 → 일반적인 변환
ex) TypeA
↔ TypeB
Format: 데이터를 특정 출력 형식으로 변환 → 포맷팅
ex) Object
→ String