백엔드

[Spring Boot] JPA 하면서 Entity 와 DTO의 변환 관계

bumheeeee 2025. 7. 11. 22:54

 

서론

 

 

 

 

DTO랑 Entity 관련 정리를 해봤다

최근에 비동기 처리, 스프링부트, 실습 중심의 구조만 만지다가 코드적으로 점점 무뎌지는 게 느껴졌다.
“이게 맞나?” 싶을 때, 구현 문제도 풀어보고, 내가 지금까지 써온 코드 방식도 되돌아보는 시간이 필요한 것 같아서 이번에는 실무에서 자주 접했던 DTO와 Entity 변환에 대해 한번 정리해봤다.

 

특히 이번 방학 때 다시 Spring을 복습과 실습을 하기 위해 위대한 김영한님의 강의도 함께 들을 예정이다.

 

 


 

본론

 

Entity와 DTO? 

 

Entity는 말 그대로 DB랑 1:1로 연결되어 있는 핵심 객체다.
JPA에서는 이걸 기준으로 테이블을 만들고, 스키마도 바뀐다. 이게 곧 실제 데이터인 셈.

DTO는 다르다.
DTO(Data Transfer Object)는 이름 그대로 계층 간 데이터를 주고받기 위해 만든 객체다.
화면에 필요한 데이터만 추려서 보내기 위한 목적.
즉, 실무에서는 Entity를 직접 클라이언트한테 노출하는 건 절대 금지다.

 

그럼 변환은 어디서 해야 해?

 

처음에는 아무 생각 없이 Controller에서 변환했다.
DTO → Entity, Entity → DTO 이런 거 그냥 toEntity() 때려주고, new ResponseDto(entity) 이런 식으로 변환하고 넘겼다.

근데 문제는, Controller가 복잡해진다.

Controller는 단순히 요청 받고 응답 보내는 역할인데, 비즈니스 로직이 하나둘 섞이기 시작하면… 유지보수 지옥이 펼쳐진다.
게다가 여러 Service에 의존하게 되고, 테스트도 어려워진다.

그래서 바꿨다.
Service Layer에서 DTO ↔ Entity 변환을 처리하는 방식으로.

 

 

도대체 DTO, Entity 변환을 "왜" 해야 할까?

 

개발할 때는 한 줄이라도 줄이고 싶고, Entity를 그냥 던지고 싶은 마음이 굴뚝같다.
하지만… 그게 장기적으로는 치명적 문제가 될 수 있다.

 

보안 문제

 

Entity에는 민감한 정보 (예: 비밀번호, 내부 필드 등)가 들어있다.
이걸 그대로 응답에 실어버리면, 의도치 않게 중요한 정보가 외부로 노출될 수도 있다.

 

유지보수 이슈

 

Entity는 DB 구조가 바뀌면 바로 영향을 받는다.
즉, 필드 하나만 바꿔도 API 응답 구조 전체가 흔들릴 수 있다.
하지만 DTO는 표현 계층(View)에만 맞춰서 따로 설계할 수 있기 때문에 이런 문제를 피할 수 있다.

 

순환 참조 문제

 

JPA에서는 양방향 연관관계가 흔한데, Entity 그대로 JSON으로 직렬화하면 무한 루프가 생길 수 있다. (@JsonManagedReference, @JsonBackReference, @JsonIgnore가 자주 붙는 이유)

 

Lazy 로딩 이슈

 

FetchType.LAZY 설정된 연관 Entity를 Controller에서 접근하면 터진다.
LazyInitializationException…
그냥 DTO로 필요한 필드만 가져오는 게 정신건강에 좋다.

 

 

 

코드의 흐름?  Request → Service → Repository → Response

 

 
@PostMapping("/account") 
public AccountSignUpResponse signUp(@RequestBody @Valid AccountSignUpRequest request) { 
	return accountService.signUp(request); 
    }
 

 

@Transactional 
public AccountSignUpResponse signUp(AccountSignUpRequest request) { 
	Account entity = request.toEntity(); // DTO → Entity 
	Account saved = accountRepository.save(entity); 
	return new AccountSignUpResponse(saved); // Entity → DTO 
   }

 

 

이렇게 하면 Controller는 깔끔하게 유지되고, DTO/Entity 변환도 Service 안에서 캡슐화되어서 훨씬 안정적이다.

 

 

 

개발 공부 중간에 겪었던 문제점 (LazyInitializationException)

 

처음엔 Controller에서 Entity를 직접 들고 다니기도 했는데…
JPA에서 LAZY로 설정해둔 필드가 Controller에서 터지는 현상 발생.

 

 

"org.hibernate.LazyInitializationException: could not initialize proxy..."

 

 

이게 뭔가 했더니, Service에서 트랜잭션 끝나고 나면 Hibernate의 세션이 닫혀버려서 그 이후에 LAZY 필드 접근하려고 하면 터짐.
즉, Lazy 필드 접근은 트랜잭션 안에서 해야 함.

그래서 더더욱 Entity → DTO 변환은 Service 안에서 하는 게 안전하다는 결론을 내렸다.

 

 

 

Entity ↔ DTO 변환 예시 코드 ( 회원가입 절차 )

 

 

1. Account Entity

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String email;

    private String name;

    @Embedded
    private Password password; 

    @Builder
    public Account(String email, String name, Password password) {
        this.email = email;
        this.name = name;
        this.password = password;
    }
}

 

 

 

2. AccountSignUpRequest DTO (DTO → Entity)

@Getter
@NoArgsConstructor
public class AccountSignUpRequest {

    @NotBlank
    @Email(message = "형식을 지켜라 !")
    private String email;

    @NotBlank
    private String name;

    @NotBlank
    private String password;

    @Builder
    public AccountSignUpRequest(String email, String name, String password) {
        this.email = email;
        this.name = name;
        this.password = password;
    }

    public Account toEntity() {
        return Account.builder()
                .email(email)
                .name(name)
                .password(Password.builder().password(this.password).build())
                .build();
    }
}

 

 

3. AccountSignUpResponse DTO (Entity → DTO)

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AccountSignUpResponse {

    private String email;
    private String name;

    @Builder
    public AccountSignUpResponse(Account account) {
        this.email = account.getEmail();
        this.name = account.getName();
    }
}

 

 

4. Service Layer에서 변환 처리 예시 (DTO ↔ Entity 중심 처리)

@Service
@RequiredArgsConstructor
public class AccountService {

    private final AccountRepository accountRepository;

    @Transactional
    public AccountSignUpResponse signUp(AccountSignUpRequest requestDto) {
        Account entity = requestDto.toEntity(); // DTO → Entity
        Account saved = accountRepository.save(entity);
        return new AccountSignUpResponse(saved); // Entity → DTO
    }
}

 

 

5. Controller는 DTO만 다룬다 (단순하고 깔끔하게)

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/account")
public class AccountController {

    private final AccountService accountService;

    @PostMapping
    public ResponseEntity<AccountSignUpResponse> signUp(
            @RequestBody @Valid AccountSignUpRequest request) {

        AccountSignUpResponse response = accountService.signUp(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}

 

 

6. LazyInitializationException 발생 예시

 

잘못된 방식 (Controller에서 LAZY 필드 접근)

@GetMapping("/{id}")
public ResponseEntity<String> getAccount(@PathVariable Long id) {
    Account account = accountService.findById(id);
    // 여기서 LAZY 필드 접근 시도 → 세션 종료 후 터짐
    String pw = account.getPassword().getPassword(); // LazyInitializationException 발생
    return ResponseEntity.ok("PW: " + pw);
}

 

올바른 방식 (Service에서 DTO 변환 후 반환)

@Transactional(readOnly = true)
public AccountSignUpResponse findById(Long id) {
    Account account = accountRepository.findById(id)
        .orElseThrow(() -> new EntityNotFoundException("Account not found"));
    return new AccountSignUpResponse(account); // 미리 접근하여 DTO 변환
}

 

Service에서 변환 → 예외 방지, 구조 정리

 

Controller는 얇게 → 테스트 편하고, 역할 분명

 

DTO 설계 → 보안·성능·유지보수 다 챙김


 

결론

 

이번 정리는 내가 그동안 너무 당연하게 써왔던 Entity와 DTO 사이의 흐름을 다시 되돌아보게 해줬다.

결론은, 실무에서는 Service Layer에서 DTO ↔ Entity 변환을 일관되게 해주는 게 훨씬 낫다는 것.

불필요한 노출을 막고, 구조도 깔끔하게 유지할 수 있고, Lazy 예외 같은 것도 잡을 수 있다.

이걸 놓치면 결국 에러가 난다. (한 번은 경험해야 정신 차리는 법… 나도 그랬음 )

생각보다 내 손이 아직 녹슬진 않은 것 같아서 다행이다.