2026. 6. 4. 19:30ㆍ카테고리 없음
user 도메인 전체 레이어 구현 — Controller·Service·Repository
📋 한줄 요약
프로필 조회·닉네임 변경·회원 탈퇴 API를 위한 user 도메인 전체 레이어(Controller→Service→Repository)를 구현했다.
🎯 왜 만들었나
마이페이지에서 내 정보를 보고, 닉네임을 바꾸고, 탈퇴할 수 있어야 한다.
Controller / Service 인터페이스 / ServiceImpl / Repository / DTO 를 한 번에 만들었다.
📁 작업 순서 (파일별)
1. UserRepository.java
📦 domain/user/repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
왜 이렇게 만들었나
Spring Data JPA 메서드 이름 규칙으로 findByUsername 하나만 추가했다.
JpaRepository가 findById, save, delete 등 기본 CRUD를 모두 제공하므로 추가 작성이 거의 없다.
2. UserInfoResponseDto.java
📦 domain/user/dto
@Getter
public class UserInfoResponseDto {
private Long id;
private String username;
private String name;
private String email;
private LocalDate birthDate;
private String gender;
private String mbti;
private String region;
private String status;
public UserInfoResponseDto(User user) {
this.id = user.getId();
this.username = user.getUsername();
// ... 나머지 필드
}
}
왜 이 구조로 만들었나
Entity를 그대로 반환하면 passwordHash 같은 민감한 필드가 노출된다.
DTO를 만들어서 응답에 필요한 필드만 골라 담았다.
생성자에 User를 직접 받아서 변환하는 방식을 택했다 — Mapper 없이 단순하게 처리.
3. UserNicknameUpdateRequest.java
📦 domain/user/dto
@Getter
@NoArgsConstructor
public class UserNicknameUpdateRequest {
private String name; // 새로 변경할 닉네임
}
왜 따로 만들었나
닉네임 변경 요청에 필요한 필드는 name 하나뿐이다.
UserInfoResponseDto를 재사용하면 불필요한 필드가 섞이므로 요청 전용 DTO를 분리했다.
4. UserService.java
📦 domain/user/service
public interface UserService {
UserInfoResponseDto getProfile(Long userId);
void updateNickname(Long userId, UserNicknameUpdateRequest request);
void withdraw(Long userId);
}
왜 인터페이스를 만들었나
Controller가 구현체(UserServiceImpl)가 아닌 인터페이스에 의존하게 한다.
나중에 구현체를 교체하거나 Mock으로 테스트할 때 Controller 코드를 바꾸지 않아도 된다.
5. UserServiceImpl.java
📦 domain/user/service/impl
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // 기본값: 읽기 전용
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public UserInfoResponseDto getProfile(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다. ID: " + userId));
return new UserInfoResponseDto(user);
}
@Override
@Transactional // 쓰기 작업만 별도로 @Transactional 추가
public void updateNickname(Long userId, UserNicknameUpdateRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다. ID: " + userId));
user.updateNickname(request.getName());
}
@Override
@Transactional
public void withdraw(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다. ID: " + userId));
user.withdraw(); // Entity 메서드 호출 → status = "DELETED"
}
}
@Transactional(readOnly = true) 를 클래스에 건 이유
조회가 대부분이므로 클래스 레벨에 readOnly = true를 걸면
JPA가 변경 감지(Dirty Checking)를 건너뛰어 성능이 약간 좋아진다.
쓰기가 필요한 메서드만 @Transactional을 따로 붙여서 오버라이드한다.
user.withdraw() 처럼 Entity 메서드를 호출하는 이유
user.status = "DELETED" 를 Service에서 직접 세팅하지 않았다.
탈퇴 조건(deletedAt 기록 등)이 바뀌어도 User.withdraw() 한 곳만 수정하면 된다.
6. UserController.java
📦 domain/user/controller
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// GET /api/users/{userId}
@GetMapping("/{userId}")
public ResponseEntity<UserInfoResponseDto> getProfile(@PathVariable Long userId) {
return ResponseEntity.ok(userService.getProfile(userId));
}
// PATCH /api/users/{userId}/nickname
@PatchMapping("/{userId}/nickname")
public ResponseEntity<String> updateNickname(
@PathVariable Long userId,
@RequestBody UserNicknameUpdateRequest request) {
userService.updateNickname(userId, request);
return ResponseEntity.ok("닉네임이 성공적으로 변경되었습니다.");
}
// DELETE /api/users/{userId}
@DeleteMapping("/{userId}")
public ResponseEntity<String> withdraw(@PathVariable Long userId) {
userService.withdraw(userId);
return ResponseEntity.ok("회원 탈퇴가 완료되었습니다.");
}
}
@PatchMapping 을 쓴 이유
닉네임 하나만 바꾸는 건 리소스 일부 수정이라 PUT(전체 교체)보다 PATCH(부분 수정)가 맞다.
7. test.http
📦 src/test
### 닉네임 변경
PATCH <http://localhost:8080/api/users/1/nickname>
Content-Type: application/json
{ "name": "흑인42호" }
### 회원 탈퇴
DELETE <http://localhost:8080/api/users/1>
IntelliJ HTTP Client 파일로 Postman 없이 IDE에서 바로 API 테스트할 수 있다.
💡 배운 점
@Transactional(readOnly = true)를 클래스에 걸고 쓰기 메서드에만 @Transactional을
오버라이드하는 패턴이 표준적인 Service 작성 방식이다.
Entity에 상태 변경 메서드를 넣으면(도메인 모델 패턴) Service가 단순해지고, 조건 변경 시 수정 범위가 줄어든다.