문제 1

DB 테이블을 User, Todo 두 개로 분리해서 설계를 했다보니, 

응답 메시지로 반환해야 할 데이터는 User 테이블에도 존재하고, Todo 테이블에도 존재했다.

그런데 반환할 객체 Todo에는 User 테이블에만 존재하는 데이터의 필드가 존재하지 않는 상황이었다.

 

아래 이미지와 같이 Todo 객체는 Todo 테이블의 각 컬럼을 필드로 가지고 있다.

나는 응답할 데이터에 userId가 아닌 userName을 넣어야 했다.


분석 & 해결

가장 처음에는 해당 메서드로 전달된 요청 DTO의 userName을 그대로 객체에 넣을까를 생각했지만,

간단한 토이 프로젝트에서는 문제없이 동작한다고 해도, 나중에 userName에 대해 데이터 변환 과정이 추가된다거나 하는 상황에서는 적용할 수 없는 방식이다.

이러한 추가 기능이 생기지 않더라도 DB에 저장되기도 전의 값을 저장 이후 반환하는 객체에 담는 건 좋지 않은 방법인 것 같아 시도하지 않았다.

 

그래서 생각한 방법은 테이블은 위 이미지대로 설계해두고 Todo 객체에 userName 필드를 추가하는 것이었다.

하지만 튜터님께 이 방법을 사용해도 되냐고 여쭤봤을 때, DB 테이블과 엔티티는 구조를 통일하는 게 옳은 방식이고, 만약 다르게 구현한다면 추후에 문제가 많이 발생할 거라고 하셨다.

 

이후 조언을 얻어 해결한 방법이 Todo 객체가 아닌 DTO에 값을 바로 저장하는 방법이다.

당시 service layer는 repository layer를 호출하는 기능만을 수행하고 있었고, repository는 JDBC로 DB와 연결하는 과정이 있다보니 최대한 객체에 담아서 서비스 레이어에 전달하고, 서비스 레이어가 DTO에 담는 과정을 수행해야 한다!는 강박이 있었다.

하지만 레포지토리 레이어에서 바로 DTO에 값을 담아 반환하는 게 더 효율적이라면 당연히 그렇게 진행해야 했다.

 

이전에는 DTO의 역할이 단순히 엔티티를 캡슐화한 객체라고만 이해했는데, 프로젝트를 진행하면서 모든 형태의 응답 데이터를 DTO에 담아서 보내는 역할이라는 것을 배울 수 있었다.


결과

@Override
    public TodoWithoutIdResponseDto findById(long todoId) throws NoSuchElementException {
        List<TodoWithoutIdResponseDto> todoList = jdbcTemplate.query("select * from todo left join user on todo.user_id = user.id where todo.id = ?", joinRowMapperV2(), todoId);
        TodoWithoutIdResponseDto foundTodo = todoList.stream().findFirst().orElseThrow();

        return foundTodo;
    }

응답 메시지

 


문제 2

addTodo를 위해 userName을 전달했는데, userName이 null값으로 클라이언트에 전달되는 문제 발생


원인 분석

아래 기존 코드를 보면 하나의 레포지토리에서 user, todo 테이블과 연결하고 값을 넣는다.

먼저 user 테이블에 데이터를 저장하면, 자동으로 userId가 생성된다.

이때 생성된 userId를 사용해서 두 테이블을 조인하고, 그 결과값을 DTO에 저장하는 과정이다.

@Repository
public class TodoRepositoryImpl implements TodoRepository {
    private final JdbcTemplate jdbcTemplate;
    LocalDateTime localDateTime = LocalDateTime.now();
    Timestamp timestamp = Timestamp.valueOf(localDateTime);

    public TodoRepositoryImpl(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public TodoResponseDto addTodo(RequestPostDto dto) {
        Todo todo = new Todo(dto.getContents());
        User user = new User(dto.getUserName(), dto.getPassword());

        // 삽입할 테이블, pk 설정
        SimpleJdbcInsert insertUserInfo = new SimpleJdbcInsert(jdbcTemplate);
        insertUserInfo.withTableName("user").usingGeneratedKeyColumns("id");

        SimpleJdbcInsert insertTodoInfo = new SimpleJdbcInsert(jdbcTemplate);
        insertTodoInfo.withTableName("todo").usingGeneratedKeyColumns("id");

        // DB에 전달할 데이터를 파라미터에 저장
        Map<String, Object> userParameters = new HashMap<>();
        userParameters.put("user_name", user.getUserName());
        userParameters.put("password", user.getPassword());

        Map<String, Object> todoParameters = new HashMap<>();
        todoParameters.put("contents", todo.getContents());
        todoParameters.put("created_date", timestamp);
        todoParameters.put("updated_date", timestamp);

        // DB에 데이터 저장 후 userId, todoId 반환
        Number userId = insertUserInfo.executeAndReturnKey(new MapSqlParameterSource(userParameters));
        Number todoId = insertTodoInfo.executeAndReturnKey(new MapSqlParameterSource(todoParameters));

        // 투두 객체 가져오기
        List<TodoResponseDto> todoList = jdbcTemplate.query("select * from todo left join user on todo.user_id = user.id where todo.id = ?", joinRowMapper(), todoId);
        TodoResponseDto foundTodo = todoList.stream().findFirst().orElseThrow(); // orElseThrow() : null이면 예외 발생

        return foundTodo;
    }

 

userId를 user 테이블에서 자동 생성 후 조인을 하면, todo 테이블에도 userId가 외래키로 존재하기 때문에 값이 자동으로 매핑될 것이라고 생각했지만, 자동으로 매핑되지 않아서 생긴 문제인 것 같다.

id user_name
101 Kim
102 Lee
id contents user_id
1 todo1 101
2 todo2 null
3 todo3 102

 

각각 user, todo 테이블이라고 할 때, 위와 같이 user_id값이 null인 레코드가 있다면 조인 결과가 아래와 같아지는 것이다.

나의 경우는 모든 레코드에 대해 user_id를 따로 삽입해주지 않았으므로, 모든 결과값의 user_name이 null로 반환된다.

id contents user_id user_name
1 todo1 101 Kim
2 todo2 null null
3 todo3 102 Lee

 


해결

Number userId = insertUserInfo.executeAndReturnKey(new MapSqlParameterSource(userParameters));

Map<String, Object> todoParameters = new HashMap<>();
todoParameters.put("contents", todo.getContents());
todoParameters.put("user_id", userId);
todoParameters.put("created_date", timestamp);
todoParameters.put("updated_date", timestamp);

 

위와 같이 userId값을 직접 파라미터에 넣어서 DB에 전달해주면 값이 제대로 반환된다.

 


+ 개선한 내용 

Repository Layer의 역할 DB와 연결하고 데이터를 관리하는 코드를 서비스 레이어와 분리하기 위함도 있지만, 

레포지토리 레이어를 패키지로 관리함으로써 각각의 테이블에 접근하는 코드를 분리하는 것이다.

위의 내 코드는 레포지토리 레이어를 제대로 활용하지 않는 코드이다.

이를 개선하기 위해서 UserRepository, TodoRepository를 각각의 자바 클래스로 분리하고 서비스 레이어에서 이를 호출하도록 리팩토링했다.

// 서비스 레이어

public TodoResponseDto addTodo(TodoRequestDto dto) {
        // DB에 User 데이터 삽입해서 자동 생성되는 userId 반환
        long userId = userRepository.addUser(dto.getUserName(), dto.getPassword());

        // userId와 요청 데이터를 DB에 저장하여 Todo 생성
        return todoRepository.addTodo(dto.getContents(), userId);
    }
public class TodoRepositoryImpl implements TodoRepository {
    public TodoResponseDto addTodo(String contents, long userId) {
       ...
    }
}
public class UserRepositoryImpl implements UserRepository{
    public long addUser(String userName, String password) {
        ...
    }
}