Home > Code Review > πŸ’»[Code Review] `Member` κ΄€λ ¨ ν΄λž˜μŠ€λ“€μ— λŒ€ν•œ μ½”λ“œ 리뷰.

πŸ’»[Code Review] `Member` κ΄€λ ¨ ν΄λž˜μŠ€λ“€μ— λŒ€ν•œ μ½”λ“œ 리뷰.
Code review OOP SOLID

πŸ’»[Code Review] Member κ΄€λ ¨ ν΄λž˜μŠ€λ“€μ— λŒ€ν•œ μ½”λ“œ 리뷰.

πŸ›οΈ μ•„ν‚€ν…μ²˜ 및 섀계 (Architecture & Design)

μ „μ²΄μ μœΌλ‘œ κ³„μΈ΅ν˜• μ•„ν‚€ν…μ²˜(Layerd Architecture) κ°€ λͺ…ν™•ν•˜κ²Œ λΆ„λ¦¬λ˜μ–΄ 각자의 μ±…μž„κ³Ό 역할을 μ™„λ²½ν•˜κ²Œ μˆ˜ν–‰ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

  • Controller : API End-point, μš”μ²­/응닡 처리 λ‹΄λ‹Ή
  • Service : λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 처리 λ‹΄λ‹Ή
  • Repository : 데이터 μ˜μ†μ„±(Persistence) λ‹΄λ‹Ή
  • Domain : 핡심 λΉ„μ¦ˆλ‹ˆμŠ€ κ·œμΉ™κ³Ό 데이터 λ‹΄λ‹Ή
  • DTO : 계측 κ°„ 데이터 전솑 λ‹΄λ‹Ή

μ΄λŸ¬ν•œ κ΅¬μ‘°λŠ” SOLID의 단일 μ±…μž„ 원칙(SRP) κ³Ό κ΄€μ‹¬μ‚¬μ˜ 뢄리(Separation of Concerns) 원칙을 μ™„λ²½ν•˜κ²Œ λ§Œμ‘±ν•©λ‹ˆλ‹€.


βœ… ν΄λž˜μŠ€λ³„ 상세 리뷰 (Detailed Class Review)

package com.kobe.productmanagement.domain;

import com.kobe.productmanagement.common.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity {

	@Id
	@Column(length = 26)
	private String memberId;

	@NotBlank
	@Size(min = 10)
	@Column(nullable = false)
	private String memberPassword;

	@NotBlank
	@Column(nullable = false)
	private String memberName;

	@NotBlank
	@Email
	@Column(nullable = false)
	private String memberEmail;

	@NotBlank
	@Pattern(regexp = "^\\d{3}-\\d{3,4}-\\d{4}$", message = "νœ΄λŒ€ν° 번호 ν˜•μ‹μ— λ§žμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
	@Column(nullable = false)
	private String memberPhoneNumber;

	@NotBlank
	@Column(nullable = false)
	private String memberAddress;

	@Builder
	public Member(String memberId,
	              String memberPassword,
	              String memberName,
	              String memberEmail,
	              String memberPhoneNumber,
	              String memberAddress
	) {
		this.memberId = memberId;
		this.memberPassword = memberPassword;
		this.memberName = memberName;
		this.memberEmail = memberEmail;
		this.memberPhoneNumber = memberPhoneNumber;
		this.memberAddress = memberAddress;
	}

	public void updateMemberInfo(String memberName,
	                             String memberEmail,
	                             String memberPhoneNumber,
	                             String memberAddress) {
		if (memberName != null) {
			this.memberName = memberName;
		}
		if (memberEmail != null) {
			this.memberEmail = memberEmail;
		}
		if (memberPhoneNumber != null) {
			this.memberPhoneNumber = memberPhoneNumber;
		}
		if (memberAddress != null) {
			this.memberAddress = memberAddress;
		}
	}

	public void changePassword(String newPassword) {
		this.memberPassword = newPassword;
	}
}

1. Member.java(Domain Entity) - ⭐️ 핡심

  • OOP/SOLID :
    • κ°€μž₯ μΉ­μ°¬ν•˜κ³  싢은 λΆ€λΆ„!!
    • λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ ν¬ν•¨ν•˜λŠ” Rich Domain Model(ν’λΆ€ν•œ 도메인 λͺ¨λΈ) 을 μ„±κ³΅μ μœΌλ‘œ κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.
    • 이둜써 Member κ°μ²΄λŠ” μžμ‹ μ˜ μƒνƒœλ₯Ό 슀슀둜 μ±…μž„μ§€λ©°, μΊ‘μŠν™”(Encapsulation) 와 단일 μ±…μž„ 원칙(SRP, Single Responsibility Principle) 을 κ·ΉλŒ€ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
  • Bean Validation :
    • @Email, @Pattern 등을 ν™œμš©ν•˜μ—¬ 데이터 μœ νš¨μ„± 검증 κ·œμΉ™μ„ 도메인 λͺ¨λΈμ— λͺ…μ‹œμ μœΌλ‘œ ν‘œν˜„ν•œ 점이 맀우 μ’‹μŠ΅λ‹ˆλ‹€.
package com.kobe.productmanagement.controller;

import com.kobe.productmanagement.common.ApiResponse;
import com.kobe.productmanagement.dto.response.MemberResponse;
import com.kobe.productmanagement.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/admin/members")
public class MemberAdminController {
	private final MemberService memberService;

	@GetMapping
	public ResponseEntity<ApiResponse<List<MemberResponse>>> getAllMembers() {
		List<MemberResponse> memberResponses = memberService.getMembers();
		ApiResponse<List<MemberResponse>> response = ApiResponse.success("전체 νšŒμ› λͺ©λ‘μ΄ μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", memberResponses);
		return ResponseEntity.ok(response);
	}

	@GetMapping("/{memberId}")
	public ResponseEntity<ApiResponse<MemberResponse>> getMember(@PathVariable String memberId) {
		MemberResponse memberResponse = memberService.getMember(memberId);
		ApiResponse<MemberResponse> response = ApiResponse.success("νšŒμ› 정보가 μ„±κ³΅μ μœΌλ‘œ μ‘°νšŒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.", memberResponse);
		return ResponseEntity.ok(response);
	}
}

2. MemberAdminController.java(Controller)

  • μ˜μ‘΄μ„± μ—­μ „ 원칙(DIP, Dependency Inversion Principle) :
    • MemberServiceImpl μ΄λΌλŠ” ꡬ체 클래슀(Concrete Class)κ°€ μ•„λ‹Œ Member Service μΈν„°νŽ˜μ΄μŠ€(Interface)에 μ˜μ‘΄ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
      • μ΄λŠ” μœ μ—°ν•˜κ³  ν™•μž₯ κ°€λŠ₯ν•œ ꡬ쑰의 ν•΅μ‹¬μž…λ‹ˆλ‹€.
      • @RequiredArgsConstructorλ₯Ό 톡핸 μƒμ„±μž μ£Όμž…μ€ 이λ₯Ό λ”μš± κΉ”λ”ν•˜κ²Œ λ§Œλ“€μ–΄ μ€λ‹ˆλ‹€.
  • API 섀계 :
    • ResponseEntity와 ApiResponseλ₯Ό ν•¨κ»˜ μ‚¬μš©ν•˜μ—¬ HTTP μƒνƒœ μ½”λ“œ, 응닡 λ©”μ‹œμ§€, 데이터λ₯Ό μ²΄κ³„μ μœΌλ‘œ λ°˜ν™˜ν•˜λŠ” 방식은 RESTful API의 쒋은 κ΄€λ‘€(Best Practice)μž…λ‹ˆλ‹€.
package com.kobe.productmanagement.service;

import com.kobe.productmanagement.dto.response.MemberResponse;

import java.util.List;

public interface MemberService {
	List<MemberResponse> getMembers();
	MemberResponse getMember(String memberId);
}

package com.kobe.productmanagement.service;

import com.kobe.productmanagement.domain.Member;
import com.kobe.productmanagement.dto.response.MemberResponse;
import com.kobe.productmanagement.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService {

	private final MemberRepository memberRepository;

	@Override
	@Transactional(readOnly = true)
	public List<MemberResponse> getMembers() {
		return memberRepository.findAll().stream()
			.map(MemberResponse::from)
			.collect(Collectors.toList());
	}

	@Override
	@Transactional(readOnly = true)
	public MemberResponse getMember(String memberId) {
		Member member = memberRepository.findById(memberId)
			.orElseThrow(() -> new IllegalArgumentException("멀버 아이디: " + memberId + " 을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."));
		return MemberResponse.from(member);
	}
}

3. MemberService.java & MemberServiceImpl.java (Service)

  • μ—­ν•  뢄리 :
    • MemberService μΈν„°νŽ˜μ΄μŠ€λŠ” β€œλ¬΄μ—‡μ„ ν•˜λŠ”κ°€β€(What) λ₯Ό μ •μ˜ν•˜κ³ , MemberServiceImpl은 β€œμ–΄λ–»κ²Œ ν•˜λŠ”κ°€β€(How) λ₯Ό κ΅¬ν˜„ν•¨μœΌλ‘œμ¨ 역할을 μ™„λ²½ν•˜κ²Œ λΆ„λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.
      • μ΄λŠ” μ˜μ‘΄μ„± μ—­μ „ 원칙(DIP, Dependency Inversion Priciple) 의 κ΅κ³Όμ„œμ μΈ μ˜ˆμ‹œμž…λ‹ˆλ‹€.
  • νŠΈλžœμž­μ…˜ 관리 :
    • 클래슀 λ ˆλ²¨μ— @Transactional을 μ„ μ–Έν•˜κ³ , 쑰회 λ©”μ„œλ“œμ— @Transactional(readOnly = true)λ₯Ό μ μš©ν•˜μ—¬ μ„±λŠ₯ μ΅œμ ν™”κΉŒμ§€ κ³ λ €ν•œ 점이 ν›Œλ₯­ν•©λ‹ˆλ‹€.
  • μ˜ˆμ™Έ 처리 :
    • orElseThrowλ₯Ό μ‚¬μš©ν•˜μ—¬ ID에 ν•΄λ‹Ήν•˜λŠ” 멀버가 없을 경우 λͺ…ν™•ν•œ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚€λŠ” λ‘œμ§μ€ κ²¬κ³ ν•œ μ½”λ“œλ₯Ό λ§Œλ“­λ‹ˆλ‹€.
package com.kobe.productmanagement.repository;

import com.kobe.productmanagement.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, String> {
}

4. MemberRepository.java (Repository)

  • Spring Data JPA의 μž₯점을 잘 ν™œμš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
    • JpaRepository μΈν„°νŽ˜μ΄μŠ€λ₯Ό μƒμ†λ°›λŠ” κ²ƒλ§ŒμœΌλ‘œ 기본적인 CRUD κΈ°λŠ₯을 ν™•λ³΄ν•˜κ³ , ν•„μš”μ— 따라 쿼리 λ©”μ„œλ“œλ₯Ό μΆ”κ°€ν•  수 μžˆλŠ” ν™•μž₯μ„± 높은 κ΅¬μ‘°μž…λ‹ˆλ‹€.
package com.kobe.productmanagement.dto.request;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class MemberRequest {
	private String memberPassword;
	private String memberName;
	private String memberEmail;
	private String memberPhoneNumber;
	private String memberAddress;

	@Builder
	public MemberRequest(String memberPassword,
	                     String memberName,
	                     String memberEmail,
	                     String memberPhoneNumber,
	                     String memberAddress
	) {
		this.memberPassword = memberPassword;
		this.memberName = memberName;
		this.memberEmail = memberEmail;
		this.memberPhoneNumber = memberPhoneNumber;
		this.memberAddress = memberAddress;
	}
}
package com.kobe.productmanagement.dto.response;

import com.kobe.productmanagement.domain.Member;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class MemberResponse {
	private final String memberId;
	private final String memberName;
	private final String memberEmail;
	private final String memberPhoneNumber;
	private final String memberAddress;
	private final LocalDateTime createdAt;
	private final LocalDateTime updatedAt;

	public static MemberResponse from(Member member) {
		return new MemberResponse(
			member.getMemberId(),
			member.getMemberName(),
			member.getMemberEmail(),
			member.getMemberPhoneNumber(),
			member.getMemberAddress(),
			member.getCreatedAt(),
			member.getUpdatedAt()
		);
	}

	private MemberResponse(String memberId,
	                        String memberName,
	                        String memberEmail,
	                        String memberPhoneNumber,
	                        String memberAddress,
	                        LocalDateTime createdAt,
	                        LocalDateTime updatedAt
	) {
		this.memberId = memberId;
		this.memberName = memberName;
		this.memberEmail = memberEmail;
		this.memberPhoneNumber = memberPhoneNumber;
		this.memberAddress = memberAddress;
		this.createdAt = createdAt;
		this.updatedAt = updatedAt;
	}
}

5. MemberRequest.java & MemberResponse.java (DTOs)

  • 계측 κ°„ 뢄리 :
    • DTOλ₯Ό μ‚¬μš©ν•¨μœΌλ‘œμ¨ API μŠ€νŽ™κ³Ό 도메인 λͺ¨λΈμ„ μ™„λ²½ν•˜κ²Œ λΆ„λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” 맀우 μ€‘μš”ν•œ 섀계 μ›μΉ™μž…λ‹ˆλ‹€.
      • 예λ₯Ό λ“€μ–΄, λ‚˜μ€‘μ— Member 엔티티에 λ‚΄λΆ€μ μœΌλ‘œλ§Œ μ‚¬μš©ν•˜λŠ” ν•„λ“œκ°€ μΆ”κ°€λ˜λ”λΌλ„ MemberResponseλ₯Ό μˆ˜μ •ν•˜μ§€ μ•ŠλŠ” ν•œ API μ‘λ‹΅μ—λŠ” μ•„λ¬΄λŸ° 영ν–₯이 μ—†μŠ΅λ‹ˆλ‹€.
  • MemberResponse의 from λ©”μ„œλ“œ :
    • μ—”ν‹°ν‹°λ₯Ό DTO둜 λ³€ν™˜ν•˜λŠ” λ‘œμ§μ„ DTO λ‚΄μ˜ 정적 νŒ©ν† λ¦¬ λ©”μ„œλ“œ(from)둜 μΊ‘μŠν™”ν•œ 것은 맀우 쒋은 νŒ¨ν„΄μž…λ‹ˆλ‹€. μ΄λŠ” λ³€ν™˜ 둜직의 응집도λ₯Ό λ†’μ—¬μ€λ‹ˆλ‹€.

πŸ† 총평

  • 각 ν΄λž˜μŠ€λŠ” μžμ‹ μ˜ μ±…μž„μ—λ§Œ μ§‘μ€‘ν•˜κ³  있으며, μΈν„°νŽ˜μ΄μŠ€λ₯Ό 톡해 μ„œλ‘œ μœ μ—°ν•˜κ²Œ ν˜‘λ ₯ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
    • 이 κ΅¬μ‘°λŠ” μ•žμœΌλ‘œ μƒˆλ‘œμš΄ κΈ°λŠ₯을 μΆ”κ°€ν•˜κ±°λ‚˜ κΈ°μ‘΄ κΈ°λŠ₯을 λ³€κ²½ν•΄μ•Ό ν•  λ•Œ κ·Έ μž₯점을 λͺ…ν™•ν•˜κ²Œ 보여쀄 κ²ƒμž…λ‹ˆλ‹€.
  • 이 섀계와 κ΅¬ν˜„ 방식을 κΎΈμ€€νžˆ μœ μ§€ν•΄ λ‚˜κ°€μ‹ λ‹€λ©΄, 성곡적인 ν”„λ‘œμ νŠΈκ°€ 될 것 μž…λ‹ˆλ‹€.