[CowAPI] 6-1. User 코드 리뷰

준돌·2022년 5월 30일
0

오늘의 Cow

목록 보기
8/45
post-thumbnail

1. [CowAPI] User 코드 리뷰


  • 게시글 하나에 작성하기에는 양이 많아져서 코드 리뷰를 분리했습니다.
  • 첫 Domain인 User에 대한 게시글이기 때문에 내용이 많습니다.
  • 자세한 코드는 Github에서 확인하실 수 있습니다.

2. 😎 Dirty Checking 사용


Understanding dirty checking in Hibernate

  • Hibernate에서는 dirty checking을 지원합니다.
  • 만약 몇몇의 필드가 수정되었을 때, hibernate는 자동적으로 SQL UPDATE를 수행합니다.
  • ❗ update data를 할때 entity를 retrieve한 후에 session.save()를 명시적으로 하지 않아도 된다는 이점이 있습니다.
  • @DynamicUpdate 어노테이션을 사용하지 않으면 모든 필드에 대해서 UPDATE SQL을 수행합니다.
  • 해당 어노테이션을 사용하면 수정된 필드만을 UPDATE 합니다.

Hibernate dirty checking method execution flow

  • hibernate 더티체킹의 execution flow 입니다.
  • SQL을 통해 DB에 요청들을 보냅니다.
  • 이러한 요청들로 인해 JPA 영속성 컨텍스트의 1차 캐시에 객체들이 쌓입니다.
  • 트랜잭션이 커밋을 실행합니다.
  • flush를 실행합니다.
  • DB에 flush를 하는 이벤트가 발생하면 DefaultFlushEntityEventLinstener.java가 동작합니다.
  • DefaultFlushEntityEventLinstener를 통해 update 사항이 있다면 dirty check를 합니다.

Flushing the Session

sess = sf.openSession();
Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); // allow queries to return stale state

Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);

// might return stale data
sess.find("from Cat as cat left outer join cat.kittens kitten");

// change to izi is not flushed!
...
tx.commit(); // flush occurs
sess.close();
  • 트랜잭션의 커밋과 세션의 종료 과정입니다.

😎 SpringBoot

  • @RequiredArgsConstructor을 통해 UserRepository를 생성자 주입합니다.

@Service
@RequiredArgsConstructor
public class UserService {
	private final UserRepository userRepository;
    
    public void update(String email) {
    	User user = userRepository.findByEmail(email);
        user.setPassword("modify the password");
        userRepository.save(user);
    }
    
    ...
}
  • 기존의 Update code 입니다.
  • findByEmail을 하여 User entity를 가져오고 save를 하여 변경사항을 저장 합니다.

@Service
@RequiredArgsConstructor
public class UserService {
	private final UserRepository userRepository;
    
    public void update(String email) {
    	User user = userRepository.findByEmail(email);
        user.setPassword("modify the password");
    }
    
    ...
}
  • Dirty Chenking을 통한 수정된 코드 입니다
  • findByEmail을 하여 User entity를 가져오고 Entity를 수정합니다.
  • hibernate가 Dirty Checking을 하여 DB에 flush 합니다.

😎 두 번의 query를 날리던 코드가 한번의 query로 해결할 수 있습니다.


3. 코드 리뷰

요구사항 분석

  • 권한은 관리자와 일반 사용자가 있습니다. (O)
    😎 Admin column으로 구분합니다.

  • 회원가입, 로그인, 회원정보 수정, 삭제가 가능합니다. (O)
    😎 RestAPI를 공부했던 것을 바탕으로 구현했습니다.

  • JWT 토큰으로 특정 사용자를 구분합니다. (O)
    😎 여기 에서 확인할 수 있습니다.

  • OAuth로 로그인이 가능합니다. (x)
    😎 JWT와 OAuth 대한 내용은 양이 많을 것 같아 따로 블로그를 게시하겠습니다.


코드 분석

lombok

  • 롬복 기능은 중복하여 많이 사용하여 따로 빼두어 분석하겠습니다.

😎 코드를 깔끔하게 만들기 위해 롬복을 사용합니다.


@Getter, @Setter

  • 어떤 필드에도 @Getter, @Setter를 어노테이트 할 수 있습니다.

  • AccessLevel를 명시적으로 지정하지 않을 경우 public으로 getter/setter method를 만듭니다.

  • 클래스에도 @Getter, @Setter를 어노테이트 할 수 있습니다.

  • non-static field들에 모두 어노테이션됩니다.

  • 수동으로 AccessLevel을 조정하여 비활성 할 수 있습니다.

😎 Getter와 Setter를 사용하여 안전하게 캡슐화 하기 위해 사용합니다.


@Builder, @Singular

  • @Builder는 클래스, 생성자, 메소드에 위치할 수 있습니다.
  • 클래스와 생성자에 일반적으로 사용합니다.
  • @Singular를 사용하면 컬렉션에 원소를 하나씩 넣을 수 있습니다.

😎 빌더 패턴을 사용하여 복잡한 필드를 관리하기 위해 사용합니다.


@AllArgsConstructor

  • 클래스에 있는 모든 필드를 받는 생성자를 생성합니다.

😎 모든 필드를 받는 생성자를 만들기 위해 사용합니다.


@NoArgsConstructor

  • @NoArgsConstructor은 파라미터가 없는 생성자를 생성합니다.
  • final 필드를 갖고 있어 만약 실패할 경우 컴파일러는 에러를 반환합니다.
  • @Data와 같이 쓰면 효율적으로 쓸 수 있습니다.

😎 파라미터가 없는 생성자를 생성하기 위해 사용합니다.


@RequiredArgsConstructor

  • 파라미터가 1개인 생성자를 생성합니다.
  • 초기화되지 않은 모든 final 필드 뿐만 아니라 초기화되지 않은 @NotNULL 필드를 파라미터로 가져옵니다.
  • @NotNull 필드는 null 체크도 생성됩니다.

😎 각각의 필드를 하나씩 받는 생성자를 만들기 위해 사용합니다.


Controller

@Api(tags = {"사용자"})

  • swagger resource class라는 것을 표시합니다.
  • Swagger는 @Api 어노테이션이 있는 클래스만 포함합니다.

😎 해당 Controller를 Swagger에 포함하기 위해 사용합니다.


@RequestMapping(value = "/api/v1")

  • web request들을 매핑하기 위한 어노테이션 입니다.
  • 클래스와 메소드 level에서 사용할 경우 HTTP method와 매핑됩니다.

😎 /api/v1을 사용하여 버전을 확인하기 쉽고 v2의 개발을 편리하게 하기 위해 사용합니다.


@RestController

  • @Controller + @ResponseBody 인 어노테이션입니다.
  • @RequestMapping 메소드가 기본적으로 @ResponseBody를 의미를 가정하는 컨트롤러로 처리됩니다.

😎 View를 반환하는 것이 아닌 JSON 형태로 응답하기 위해 사용합니다.
😎 ResponseEntity를 이용하여 HTTP 응답으로 반환하는 것으로 수정해볼 예정입니다.


@RequestHeader

  • 매개변수가 web request header와 바인딩 하기 위해 사용합니다.

😎 jwt 토큰을 HTTP 헤더로 받기 위해 사용합니다.


@RequestBody

  • 매개변수와 web request body와 바인딩합니다.
  • HttpMessageConverter를 통해 전달 됩니다.
  • @Valid로 유효성을 검사할 수 도 있습니다.

😎 User의 HTTP body를 받기 위해 사용합니다.

UserController.java

@Api(tags = {"사용자"})
@RequestMapping(value = "/api/v1")
@RestController
@RequiredArgsConstructor
public class UserController {
	private final UserService userService;
    
    // 회원가입
    @PostMapping("/users/new-user")
    public UserDto signUp(@RequestBody UserDto userDto) {
        return userService.signUp(userDto);
    }

    // 로그인
    @PostMapping("/users/user")
    public UserDto signIn(@RequestBody UserDto userDto) {
        return userService.signIn(userDto);
    }

    // 회원 수정
    @PutMapping("/users/user")
    public UserDto updateUser(@RequestHeader("Authorization") String userToken, @RequestBody UserDto userDto) {
        return userService.updateUser(userToken, userDto);
    }

    // 회원 삭제
    @DeleteMapping("/users/user")
    public UserDto deleteUser(@RequestHeader("Authorization") String userToken, @RequestBody UserDto userDto) {
        return userService.deleteUser(userToken, userDto);
    }

😎 API 문서 : Swagger
😎 회원 수정과 로그인시에는 Jwt 토큰을 받습니다.


Service

@Service

  • service라는 것을 나타내는 어노테이션 입니다.
  • 원래는 DDD (Domain Driven Design) 패턴에서 말하는 "캡슐화된 상태 없이 모델에서 독립적으로 제공되는 인터페이스로 제공되는 작업"을 의미합니다.
  • @Component의 특정화 한것으로써 classpath scanning을 통해서 autodetect 됩니다.

😎 User domain의 Service 계층을 나타내기 위해 사용합니다.
😎 Service를 Bean에 등록하기 위해 사용합니다.


@Transactional

😎 ResponseStatusException은 RuntimeException이므로 Transaction이 제대로 동작합니다.
😎 변경 (Transactional 삭제)

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
	
    ...

	@Transactional
	@Override
	public <S extends T> S save(S entity) {

		Assert.notNull(entity, "Entity must not be null.");

		if (entityInformation.isNew(entity)) {
			em.persist(entity);
			return entity;
		} else {
			return em.merge(entity);
		}
	}
    
    ...
    
}

😎 위와 같이 구현체인 SimpleJpaRepository에서 이미 Transactional을 어노테이션 한다는 것이 확인되어 불필요한 Transactional 어노테이션을 삭제 합니다.


UserService.java

@Service
@RequiredArgsConstructor
@Transactional
public class UserService {
    private final UserRepository userRepository;

    public UserDto signUp(UserDto userDto) {
        if(userRepository.existsByEmail(userDto.getEmail())) throw new ResponseStatusException(CONFLICT, "이미 가입되어 있는 유저입니다");
        userDto.setCreatedUser();
        User user = UserDto.toEntity(userDto);
        return UserDto.of(userRepository.save(user));
    }

    public UserDto signIn(UserDto userDto) {
        User foundUser = userRepository.findByEmail(userDto.getEmail()).orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "존재하지 않는 사용자 입니다."));
        if(!foundUser.getPassword().equals(userDto.getPassword())) throw new ResponseStatusException(BAD_REQUEST, "비밀번호가 틀렸습니다.");
        return UserDto.of(foundUser);
    }

    public UserDto updateUser(String userToken, UserDto userDto) {
        User user = userRepository.findByEmail(userDto.getEmail()).orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "존재 하지 않는 유저입니다."));
        userRepository.updateUser(user, userDto);
        return UserDto.of(user);
    }

    public UserDto deleteUser(String userToken, UserDto userDto) {
        User user = userRepository.findByEmail(userDto.getEmail()).orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "존재 하지 않는 유저입니다."));
        userRepository.deleteUser(user);
        return UserDto.of(user);
    }
}

😎 RestAPI를 통해 요청받은 UserController에서 각각의 기능의 UserService를 호출합니다.
😎 호출된 UserService는 기능에 따른 로직을 수행합니다.
😎 UserRepository를 통해 DB의 내용을 수정합니다.
😎 DTO를 UserController에 반환합니다.


Domain

Entity

@Entity

  • 클래스가 엔티티 임을 알려주는 어노테이션 입니다.

😎 User Table과 매핑하기 위해 사용합니다.


@DynamicUpdate

  • 변경된 column들을 Update 하기 위해 동적 sql 생성을 사용합니다.
  • Update 전에 select 없이는 가능하지 않습니다.

😎 Dirty checking을 사용하여 업데이트 하기 위해 사용합니다.


UserEntity.java

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@DynamicUpdate
public class User {
    @Id
    private String email;

    @Column
    @NotNull
    private String password;

    @Column
    @NotNull
    private Boolean admin;

    @Column
    @NotNull
    private Boolean isDeleted;

    @Column
    @NotNull
    private Timestamp createdDate;

    @Column
    @NotNull
    private Timestamp updatedDate;

    @Column
    private Timestamp deletedDate;

    @Column
    @NotNull
    private String creator;

    @Column
    private String updater;
}

😎 UserTable과 매핑되는 Entity입니다.


Repository

@Repository

  • 레포지토리 임을 알려주는 어노테이션 입니다.
  • DDD (Domain-Driven Design)에서 "저장, 탐색, 검색 행위를 모방하는 객체들의 컬렉션을 위한 메커니즘" 입니다.
  • 전통적으로 Java의 EE 패턴들을 구현하는 팀들은 DAO (Data Access Object" 클래스에 이 sterotype을 적용합니다.

😎 User domain의 Repository를 Bean에 등록하기 위해 사용합니다.


JpaRepository

  • JPA 의 확장 레포지토리 입니다.

😎 SpringDataJpa를 사용하기 위해 사용합니다.
😎 SimpleJpaRepository를 상속하여 사용하는 방법을 고안중입니다.


UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User, String> {
    boolean existsByEmail(Object email);
    Optional<User> findByEmail(Object email);

    default void updateUser(User user, UserDto userDto) {
        if(userDto.getPassword() != null) user.setPassword(userDto.getPassword());
        if(userDto.getAdmin() != null) user.setAdmin(userDto.getAdmin());
        if(userDto.getIsDeleted() != null) user.setIsDeleted(userDto.getIsDeleted());
        if(userDto.getCreatedDate() != null) user.setCreatedDate(userDto.getCreatedDate());
        if(userDto.getUpdatedDate() != null) user.setUpdatedDate(Timestamp.valueOf(LocalDateTime.now()));
        if(userDto.getDeletedDate() != null) user.setDeletedDate(userDto.getDeletedDate());
        if(userDto.getCreator() != null) user.setCreator(userDto.getCreator());
        if(userDto.getUpdater() != null) user.setUpdater("API");
    }

    default void deleteUser(User user) {
        if(user.getIsDeleted().equals(false)) {
            user.setIsDeleted(true);
            user.setDeletedDate(Timestamp.valueOf(LocalDateTime.now()));
            user.setUpdater("API");
        }
        else {
            throw new ResponseStatusException(BAD_REQUEST, "이미 삭제된 유저 입니다.");
        }
    }
}

😎 DB에 접근하는 DAO 입니다.
😎 updateUser와 deleteUser는 Dirty Checking을 통해 DB의 User를 수정합니다.


Test Code

😎 [CowAPI] 2-1. TDD Code review를 기반으로 테스트 코드를 작성했습니다.

😎 TestCode에서 고려해봐야 할점

  • Test Code의 빠른 속도를 위해 reflection의 사용을 줄이는 방법 (ex, @Autowired)
  • 생성자 주입을 했을 때, Mock을 이용한 테스트가 필요한가??
  • Sucess case와 Fail case를 나눈 테스트 코드 필요

4. Code


profile
눈 내리는 겨울이 좋아!

0개의 댓글