어떤 블로그에서는 DTO라고 정의하는 반면, 다른 블로그에서는 VO로 정의하여 사용하는 경우가 있는데 어떤 차이가 있을까? 그리고 DAO, Entity는 무엇을 의미하는 용어일까?
DAO(Data Access Object)는 데이터베이스의 데이터에 접근하기 위한 객체로 직접 데이터베이스에 접근하여 데이터를 삽입, 삭제, 조회 등을 조작할 수 있는 기능을 수행한다. 데이터베이스에 접근하기 위한 로직과 비즈니스 로직을 분리하기 위해 사용한다.
public class UserDao {
public void add(UserDto dto) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/test", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("insert into users(id,name,password) value(?,?,?)");
preparedStatement.setString(1, dto.getName());
preparedStatement.setInt(2, dto.getValue());
preparedStatement.setString(3, dto.getData());
preparedStatement.executeUpdate();
preparedStatement.close();
connection.close();
}
}
다만 MyBatis 같은 프레임워크를 사용하면 커넥션 풀을 제공하기 때문에 DAO를 별도로 만드는 경우는 드물다. (JPA에서의 Repository의 기능과 동일한 역할을 수행한다고 볼 수 있겠다.)
DTO(Data Transfer Object)는 각 계층간(Controller, View, Business Layer(Model)) 데이터 교환을 위한 객체다. 로직을 가지지 않는 데이터 객체이고 Getter와 Setter 메서드만 가진 클래스를 의미한다. 요청이나 응답 값을 전달하는 용도로 많이 사용한다.
@Getter
@Setter
public class UserDto {
private String email;
private String password;
}
조금 더 실무에 맞게 구체적으로 DTO 클래스를 구현해본다면 하단의 코드가 된다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
@NotBlank
@Pattern(regexp = "^([\\w-]+(?:\\.[\\w-]+)*)@((?:[\\w-]+\\.)*\\w[\\w-]{0,66})\\.([a-z]{2,6}(?:\\.[a-z]{2})?)$")
private String email;
@JsonIgnore
@NotBlank
@Size(min = 4, max = 15)
private String password;
@NotBlank
@Size(min = 6, max = 10)
private String name;
public User toEntity() {
return new User(email, password, name);
}
public User toEntityWithPasswordEncode(PasswordEncoder bCryptPasswordEncoder) {
return new User(email, bCryptPasswordEncoder.encode(password), name);
}
}
💡 왜 Setter를 사용하지 않았을까?
첫 번째 이유는 Setter 메서드를 이용하게 되면 어떤 의도를 가지고 있는지를 확인하기 어렵기 때문이다. Setter 메서드를 이용하면 일반적으로
set*()
형태의 메서드를 이용하게 되는데, 이러한 형태의 메서드는 가독성이 떨어지고 어떤 용도로 이 메서드를 호출했는지 명확하게 파악하기 힘들다는 단점을 가지고 있다.두 번째 이유는 객체의 일관성을 유지하기 어렵다는 점이다. Setter는 어떤 곳에서 접근이 가능하도록 자바 빈 규약에 정의되어있다. 즉, 어디서든 접근할 수 있기에 개발할 때 의도치 않은 변경을 수행할 수 있다는 의미다.
따라서 Setter를 지양하고 생성자 오버로딩이나 Builder 패턴을 이용하는 것을 권장한다.
VO(Value Object)는 DTO와 다르게 불변 클래스로, Read-Only 속성만을 가지고 있다. 즉, DTO에서는 Getter와 Setter 메서드 둘 다 존재할 때, VO는 Setter 성격을 가지고 있는 메서드는 가져서는 안되며 오로지 생성자로만 값을 초기화해야하고 Getter 성격의 메서드만 사용한다. 추가적으로, VO는 불변 객체이기에 equals()
와 hashcode()
를 오버라이딩하는 것은 필수라고 볼 수 있다.
대부분 DTO와 VO를 같은 개념으로 프로젝트에서 사용하지만, 명확하게 구분짓어서 사용하는 편이 맞다고 생각한다. 즉, DTO는 데이터 전달용으로, VO는 값 표현용으로 사용하는 것이 정확하다.
대표적인 예시로는 주민등록번호가 되겠다. 정말 특별한 케이스가 아닌 이상 변하지 않으니, 여기서는 절대 변경되지 않는다고 가정해보자. 주민등록번호에 대한 내용을 하나의 클래스로 표현한다면 다음과 같다.
@Getter
@EqualsAndHashCode
public class ResidentRegistrationNumber {
public String number;
public ResidentRegistrationNumber {
this.number = number;
}
}
Entity 클래스는 실제 데이터베이스의 테이블과 1:1로 매핑되는 객체로 데이터베이스 테이블내에 존재하는 컬럼만을 속성으로 가진다. Entity 클래스에서는 도메인 로직만을 가지고 있어야 하고 서비스 로직은 가지고 있으면 안된다. 여기서 말하는 도메인 로직이란, 도메인 데이터를 조작하는 update()
와 같은 로직을 의미한다.
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class User implements Serializable {
private static final long serialVersionUID = 7342736640368461848L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@JsonProperty
private Long id;
@Column(nullable = false)
@JsonProperty
private String email;
@Column(nullable = false)
@JsonProperty
private String password;
public User updatePassword(String password) {
this.password = password;
return this;
}
}
책에서 보여준 게시판 예제에서의 서비스 구현 방법이 인상적이여서 남겨보고자 한다.
일반적으로 DAO는 데이터베이스 테이블당 하나를 만들게 되지만, 사용자에게 제공하는 서비스는 여러 테이블의 정보를 조합하여 제공하는 경우가 많아 다수의 DAO 혹은 다수의 서비스가 하나의 DAO를 사용하기도 한다. 대부분의 경우에는 하나의 서비스가 하나의 DAO의 관계를 맺는다고 한다. 또한 서비스는 DAO와의 연동만이 아닌 서버 기술이나 각 벤더별 데이터베이스에 종속되지 않는 로직을 구현하는 곳이기에 반드시 서비스를 작성하는 습관을 길러야 한다고 했다.
public interface BoardService {
public abstract List<BoardVO> list();
public abstract int delete(BoardVO boardVO);
public abstract int edit(BoardVO boardVO);
public abstract void write(BoardVO boardVO);
public abstract BoardVO read(int seq);
}
@Service
public class BoardServiceImpl implements BoardService {
@Resource
private BoardDao boardDao;
public BoardDao getBoardDao() {
return boardDao;
}
public void setBoardDao(BoardDao boardDao) {
this.boardDao = boardDao;
}
@Override
public List<BoardVO> list() {
return boardDao.list();
}
@Override
public int delete(BoardVO boardVO) {
return boardDao.delete(boardVO);
}
@Override
public int edit(BoardVO boardVO) {
return boardDao.update(boardVO);
}
@Override
public void write(BoardVO boardVO) {
return boardDao.insert(boardVO);
}
@Override
public BoardVO read(int seq) {
boardDao.updateReadCount(seq);
return boardDao.select(seq);
}
}