π»[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 μλ΅μλ μλ¬΄λ° μν₯μ΄ μμ΅λλ€.
- μλ₯Ό λ€μ΄, λμ€μ
- DTOλ₯Ό μ¬μ©ν¨μΌλ‘μ¨ API μ€νκ³Ό λλ©μΈ λͺ¨λΈμ μλ²½νκ² λΆλ¦¬νμ΅λλ€. μ΄λ λ§€μ° μ€μν μ€κ³ μμΉμ
λλ€.
-
MemberResponse
μfrom
λ©μλ :- μν°ν°λ₯Ό DTOλ‘ λ³ννλ λ‘μ§μ DTO λ΄μ μ μ ν©ν 리 λ©μλ(
from
)λ‘ μΊ‘μνν κ²μ λ§€μ° μ’μ ν¨ν΄μ λλ€. μ΄λ λ³ν λ‘μ§μ μμ§λλ₯Ό λμ¬μ€λλ€.
- μν°ν°λ₯Ό DTOλ‘ λ³ννλ λ‘μ§μ DTO λ΄μ μ μ ν©ν 리 λ©μλ(
π μ΄ν
- κ° ν΄λμ€λ μμ μ μ±
μμλ§ μ§μ€νκ³ μμΌλ©°, μΈν°νμ΄μ€λ₯Ό ν΅ν΄ μλ‘ μ μ°νκ² νλ ₯νκ³ μμ΅λλ€.
- μ΄ κ΅¬μ‘°λ μμΌλ‘ μλ‘μ΄ κΈ°λ₯μ μΆκ°νκ±°λ κΈ°μ‘΄ κΈ°λ₯μ λ³κ²½ν΄μΌ ν λ κ·Έ μ₯μ μ λͺ ννκ² λ³΄μ¬μ€ κ²μ λλ€.
- μ΄ μ€κ³μ ꡬν λ°©μμ κΎΈμ€ν μ μ§ν΄ λκ°μ λ€λ©΄, μ±κ³΅μ μΈ νλ‘μ νΈκ° λ κ² μ λλ€.