Java & Spring/SpringBoot

스프링 부트 (Spring Boot) Entity, Repository, DAO, DTO 완벽 가이드

IT개발지식창고 2025. 6. 16. 22:17
반응형

스프링 부트 (Spring Boot) Entity, Repository, DAO, DTO 완벽 가이드

현대적인 웹 애플리케이션 아키텍처 개념도

🚀 서론

스프링 부트(Spring Boot)를 이용한 웹 애플리케이션 개발에서 데이터 계층(Data Layer) 설계는 전체 시스템 아키텍처의 핵심입니다. Entity, Repository, DAO, DTO는 데이터 처리와 관련된 핵심 컴포넌트들로, 각각의 역할과 상호작용을 명확히 이해하는 것이 효율적인 애플리케이션 개발의 출발점입니다.

현대의 엔터프라이즈 애플리케이션에서는 계층형 아키텍처(Layered Architecture)를 통해 관심사의 분리(Separation of Concerns)를 구현하며, 이를 통해 코드의 재사용성, 유지보수성, 테스트 용이성을 확보할 수 있습니다.

📋 Entity (엔티티): 데이터베이스 테이블의 객체화

데이터베이스 스키마와 테이블 구조

Entity란?

Entity는 데이터베이스 테이블과 1:1로 매핑되는 클래스로, 실제 DB 테이블 내에 존재하는 컬럼들을 필드로 가지는 객체를 말합니다. Entity는 DB의 테이블과 1대 1로 대응되며, 테이블이 가지지 않는 컬럼을 필드로 가져서는 안됩니다.

Entity의 핵심 특징

  1. 데이터베이스 중심 설계: DB의 테이블내에 존재하는 컬럼만을 속성(필드)으로 가져야 합니다
  2. 상속 제한: Entity 클래스는 다른 클래스를 상속받거나 인터페이스의 구현체여서는 안됩니다
  3. 도메인 로직 집중: 최대한 외부에서 Entity 클래스의 getter method를 사용하지 않도록 해당 클래스 안에서 필요한 로직 method을 구현합니다

Entity 구현 예제

@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 50)
    private String name;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @CreatedDate
    private LocalDateTime createdAt;
    
    @LastModifiedDate
    private LocalDateTime updatedAt;
    
    @Builder
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    // 비즈니스 로직 메서드
    public void updateEmail(String newEmail) {
        this.email = newEmail;
    }
    
    public boolean isValidUser() {
        return name != null && !name.trim().isEmpty() 
               && email != null && email.contains("@");
    }
}

주요 어노테이션 설명

  • @Entity: Spring JPA에서 객체(class)를 entity로 사용하기 위해 @Entity annotation을 객체 위에 붙여줍니다
  • @Id: entity는 primary key가 꼭 필요하므로 PK로 사용할 변수 위에 @Id를 붙여 PK로 지정해줍니다
  • @GeneratedValue: ID의 값을 entity가 생성될 때마다 자동 생성되게 해주는 annotation입니다

🗄️ Repository: 데이터 접근 계층의 추상화

데이터 저장소와 접근 계층

Repository란?

Repository는 Spring Boot에서 Entity에 의해 생성된 DB에 접근하는 메서드들을 사용하기 위한 인터페이스입니다. JPA에서 DB에 데이터를 CRUD 하는 Repository 객체들이 DAO라고 볼 수 있습니다.

Repository vs DAO의 차이점

DAO는 실제 영구 저장소에 접근하는데, 이때 계층이 깨지지 않기 위해서 DTO를 사용합니다. Repository는 객체의 상태를 관리하는 저장소입니다.

Repository 구현 예제

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    // 메서드 이름으로 쿼리 생성
    Optional<User> findByEmail(String email);
    
    List<User> findByNameContaining(String name);
    
    // @Query 어노테이션을 사용한 커스텀 쿼리
    @Query("SELECT u FROM User u WHERE u.createdAt > :date")
    List<User> findUsersCreatedAfter(@Param("date") LocalDateTime date);
    
    // 네이티브 쿼리 사용
    @Query(value = "SELECT * FROM users WHERE email LIKE %:domain%", 
           nativeQuery = true)
    List<User> findByEmailDomain(@Param("domain") String domain);
    
    // 수정 쿼리
    @Modifying
    @Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
    int updateUserName(@Param("id") Long id, @Param("name") String name);
}

JpaRepository의 주요 메서드들

JpaRepository 인터페이스에 정의된 메서드들:

  • save(): 엔티티 저장 또는 수정
  • findById(): ID로 엔티티 조회
  • findAll(): 모든 엔티티 조회
  • deleteById(): ID로 엔티티 삭제
  • count(): 엔티티 개수 조회

💾 DAO (Data Access Object): 데이터 접근 객체

데이터베이스 연결과 데이터 액세스

DAO란?

DAO는 실제로 DB에 접근하는 객체로, Service와 DB를 연결하는 고리의 역할을 합니다. DAO는 데이터베이스에 접근하기 위한 로직과 목표하는 비즈니스를 처리하기 위한 로직을 분리시키기 위해 사용합니다.

DAO의 특징

  1. Persistence Layer: DB에 data를 CRUD하는 계층
  2. SQL 사용: SQL를 사용(개발자가 직접 코딩)하여 DB에 접근한 후 적절한 CRUD API를 제공
  3. 계층 분리: 데이터베이스 접근 로직과 비즈니스 로직의 분리

DAO 구현 예제

@Repository
@Transactional
public class UserDAO {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    public User save(User user) {
        if (user.getId() == null) {
            entityManager.persist(user);
            return user;
        } else {
            return entityManager.merge(user);
        }
    }
    
    public Optional<User> findById(Long id) {
        User user = entityManager.find(User.class, id);
        return Optional.ofNullable(user);
    }
    
    public List<User> findAll() {
        return entityManager.createQuery(
            "SELECT u FROM User u", User.class)
            .getResultList();
    }
    
    public void deleteById(Long id) {
        User user = entityManager.find(User.class, id);
        if (user != null) {
            entityManager.remove(user);
        }
    }
    
    public List<User> findByCustomCriteria(String name, String email) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = cb.createQuery(User.class);
        Root<User> root = query.from(User.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        if (name != null) {
            predicates.add(cb.like(root.get("name"), "%" + name + "%"));
        }
        if (email != null) {
            predicates.add(cb.equal(root.get("email"), email));
        }
        
        query.where(predicates.toArray(new Predicate[0]));
        return entityManager.createQuery(query).getResultList();
    }
}

📦 DTO (Data Transfer Object): 데이터 전송 객체

계층간 데이터 전송과 통신

DTO란?

DTO는 DB에서 데이터를 얻어 Service나 Controller 등으터 보낼 때 사용하는 객체로, 로직을 갖고 있지 않는 순수한 데이터 객체이며, getter/setter 메서드만을 갖습니다.

DTO의 목적

  1. 계층간 데이터 전송: 계층간(Controller, View, Business Layer) 데이터 전송을 위한 JavaBeans
  2. Entity 보호: Entity와 DTO를 분리해서 관리해야 하는 이유는 DB Layer와 View Layer 사이의 역할을 분리하기 위해서
  3. API 응답 최적화: 클라이언트에게 필요한 데이터만 전송

DTO 구현 예제

Request DTO

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserCreateRequestDto {
    
    @NotBlank(message = "이름은 필수입니다")
    @Size(min = 2, max = 50, message = "이름은 2-50자 사이여야 합니다")
    private String name;
    
    @Email(message = "올바른 이메일 형식이 아닙니다")
    @NotBlank(message = "이메일은 필수입니다")
    private String email;
    
    // DTO를 Entity로 변환하는 메서드
    public User toEntity() {
        return User.builder()
                .name(this.name)
                .email(this.email)
                .build();
    }
}

Response DTO

@Getter
@Builder
@AllArgsConstructor
public class UserResponseDto {
    
    private Long id;
    private String name;
    private String email;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // Entity를 DTO로 변환하는 정적 메서드
    public static UserResponseDto from(User user) {
        return UserResponseDto.builder()
                .id(user.getId())
                .name(user.getName())
                .email(user.getEmail())
                .createdAt(user.getCreatedAt())
                .updatedAt(user.getUpdatedAt())
                .build();
    }
}

Entity와 DTO 분리의 이유

Entity는 실제 테이블과 매핑되어 변경되게 되면 여러 다른 Class에 영향을 끼치고, DTO는 View와 통신하며 자주 변경되므로 분리해주어야 합니다. 또한 Domain Model을 아무리 잘 설계했다고 해도 각 View 내에서 Domain Model의 getter만을 이용해서 원하는 정보를 표시하기가 어려운 경우가 종종 있습니다.

🏗️ 스프링 부트 계층 구조와 데이터 흐름

시스템 아키텍처와 계층별 흐름

전체 아키텍처 개요

Controller Layer (Presentation)
        ↕ (DTO)
Service Layer (Business Logic)
        ↕ (Entity)
Repository/DAO Layer (Data Access)
        ↕ (Entity)
Database Layer (Persistence)

실제 구현 예제

Controller

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    private final UserService userService;
    
    @PostMapping
    public ResponseEntity<UserResponseDto> createUser(
            @Valid @RequestBody UserCreateRequestDto requestDto) {
        UserResponseDto responseDto = userService.createUser(requestDto);
        return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserResponseDto> getUser(@PathVariable Long id) {
        UserResponseDto responseDto = userService.getUserById(id);
        return ResponseEntity.ok(responseDto);
    }
    
    @GetMapping
    public ResponseEntity<List<UserResponseDto>> getAllUsers() {
        List<UserResponseDto> users = userService.getAllUsers();
        return ResponseEntity.ok(users);
    }
}

Service

@Service
@Transactional
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    
    public UserResponseDto createUser(UserCreateRequestDto requestDto) {
        // DTO를 Entity로 변환
        User user = requestDto.toEntity();
        
        // 비즈니스 로직 처리
        if (userRepository.findByEmail(user.getEmail()).isPresent()) {
            throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
        }
        
        // Entity 저장
        User savedUser = userRepository.save(user);
        
        // Entity를 DTO로 변환하여 반환
        return UserResponseDto.from(savedUser);
    }
    
    @Transactional(readOnly = true)
    public UserResponseDto getUserById(Long id) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("사용자를 찾을 수 없습니다."));
        return UserResponseDto.from(user);
    }
    
    @Transactional(readOnly = true)
    public List<UserResponseDto> getAllUsers() {
        return userRepository.findAll().stream()
                .map(UserResponseDto::from)
                .collect(Collectors.toList());
    }
}

🔧 실무 적용 팁과 Best Practices

개발 모범 사례와 코드 품질

1. Entity 설계 원칙

  • Entity는 비즈니스 로직을 포함해야 하며, 단순한 데이터 컨테이너가 되어서는 안됩니다
  • 외부에서 Entity의 상태를 직접 변경하지 못하도록 setter 사용을 지양하고 의미있는 메서드를 제공합니다
  • Entity 간의 연관관계는 신중하게 설계하고, 지연 로딩(Lazy Loading)을 기본으로 사용합니다

2. Repository 활용법

  • 복잡한 쿼리는 @Query 어노테이션을 사용하여 명시적으로 작성합니다
  • 성능이 중요한 조회의 경우 QueryDSL이나 Criteria API를 고려합니다
  • Repository 메서드 이름은 명확하고 의미를 전달할 수 있도록 작성합니다

3. DTO 설계 지침

  • Request와 Response DTO를 분리하여 API의 입력과 출력을 명확히 구분합니다
  • DTO에는 유효성 검증 어노테이션을 적절히 활용합니다
  • Entity와 DTO 간의 변환 로직은 명확하고 간단하게 구현합니다

4. 성능 최적화

  • N+1 문제를 방지하기 위해 @EntityGraph나 Fetch Join을 활용합니다
  • 대용량 데이터 처리 시에는 페이징과 정렬을 적절히 활용합니다
  • 읽기 전용 트랜잭션에는 @Transactional(readOnly = true)를 사용합니다

📈 고급 패턴과 확장

고급 개발 패턴과 확장성

CQRS (Command Query Responsibility Segregation) 적용

// Command용 DTO
@Getter
@Setter
public class UserCommandDto {
    private String name;
    private String email;
    // 명령 처리에 필요한 필드들
}

// Query용 DTO
@Getter
@Builder
public class UserQueryDto {
    private Long id;
    private String name;
    private String email;
    private LocalDateTime createdAt;
    // 조회에 필요한 모든 정보
}

Repository Pattern 확장

public interface UserRepositoryCustom {
    List<User> findUsersByComplexConditions(UserSearchCondition condition);
}

@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
    
    @PersistenceContext
    private EntityManager entityManager;
    
    @Override
    public List<User> findUsersByComplexConditions(UserSearchCondition condition) {
        // 복잡한 동적 쿼리 구현
        return new JPAQueryFactory(entityManager)
                .selectFrom(user)
                .where(buildConditions(condition))
                .fetch();
    }
}

🎯 결론

성공적인 소프트웨어 개발과 팀워크

스프링 부트에서 Entity, Repository, DAO, DTO는 각각 고유한 역할과 책임을 가지고 있으며, 이들의 올바른 활용은 확장 가능하고 유지보수가 용이한 애플리케이션 구축의 핵심입니다.

Entity는 도메인 모델의 핵심으로서 비즈니스 로직을 담당하고, Repository는 데이터 접근을 추상화하여 유연성을 제공합니다. DAO는 복잡한 데이터 접근 로직을 캡슐화하고, DTO는 계층 간 데이터 전송을 안전하고 효율적으로 처리합니다.

현대적인 스프링 부트 애플리케이션에서는 이러한 계층 분리를 통해 관심사의 분리를 달성하고, 테스트 용이성, 코드 재사용성, 시스템 확장성을 확보할 수 있습니다. 각 컴포넌트의 특성을 이해하고 적절히 활용한다면, 더욱 견고하고 효율적인 엔터프라이즈 애플리케이션을 개발할 수 있을 것입니다.


 

반응형