1주차로 작성한 스프링 부트 프로젝트에 대해 리뷰받은 내용을 정리해보려고 한다..!
1. Controller Based Injection
@RestController
@RequestMapping("/api/users")
public class UserRestController {
@Autowired
UserService userService;
public UserRestController(UserService userService){
this.userService = userService;
}
원래 내가 쓰던 코드였다. 하지만 여기서
✨ UserService는 이미 생성자를 통해서 DI(Dependency Injection)되기 때문에 따로 @Autowired 표기를 하지 않아도 된다. 이렇게 Controller based Injection을 하게되면 nullPointerException을 방지할 뿐만 아니라, UserService 필드를 final처리할 수 있다는 장점이 있다.
✨그래서 UserService 필드를 private final 처리를 하여 좀 더 안정적인 코드를 만들 수 있게되었다!
@RestController
@RequestMapping("/api/users")
public class UserRestController {
private final UserService userService;
public UserRestController(UserService userService){
this.userService = userService;
}
# 그리고 final과 static final 차이 갑자기 궁금해서 찾아보았다. [참조]
2. Code Formatting
이번에 IntelliJ를 처음 사용하는데 code formatting 기능을 잘 몰랐었다.
커스텀으로 코드 포맷팅을 하고싶으면 File > Setting > Editor > Code Style > Java 에 들어가서 원하는 기능들을 선택하면 된다.
그리고...! 설정된 포멧대로 현재 코드를 Reformat 하고싶으면 Code > Reformat Code을 하면 된다!
난 단축키 Ctrl + Alt + L 써야징 룰루
3. Consumes and Produces Setting in GET / POST method
내 코드에서는 모든 메소드의 consumes과 produces 설정을 일일이 application/json 표기를 해주었지만..
@GetMapping(path = "/{id}", consumes = "application/json", produces = "application/json")
public User findUserById(@PathVariable("id") Long id) {
return userService.findById(id);
}
@GetMapping(path = "/", consumes = "application/json", produces = "application/json")
public List<User> findAll() {
return userService.findAll();
}
@PostMapping(path = "/join", consumes = "application/json", produces = "application/json")
public UserResponseDTO createUser(@Valid @RequestBody UserRequestDTO UserDTO) {
return userService.createUser(UserDTO);
}
기본 설정이 application/json이더라..ㅎㅎ 그래서 불필요한 부분은 아래와 같이 없애주었다.
@GetMapping(path = "/{id}")
public User findUserById(@PathVariable("id") Long id) {
return userService.findById(id);
}
@GetMapping(path = "/")
public List<User> findAll() {
return userService.findAll();
}
@PostMapping(path = "/join")
public UserResponseDTO createUser(@Valid @RequestBody UserRequestDTO UserDTO) {
return userService.createUser(UserDTO);
}
4. Proper Terminology
이메일 정보를 담는 Value Object, Email을 만들어주었다.
public class Email {
@NotNull
@Email
private String address;
private String username;
private String domain;
....
}
여기서 포인트는 username! Email에 대한 정확한 terminology가 아니었다..! [참조]
위키피디아에 따르면 Email = local part + @ + domain 으로 이루어져있다.
즉, 여기서는 username보다는 localPart라는 용어가 더 적절해보인다!
public class Email {
@NotNull
@Email
private String address;
private String localPart;
private String domain;
....
}
5. Util 클래스 활용
Timestamp를 LocalDateTime으로 바꿔야해서 getLocalDataTime이라는 메소드를 만들어주었다.
public List<User> findAll() {
...
getLocalDateTime(rs.getTimestamp("last_login_at"));
...
}
private LocalDateTime getLocalDateTime(Timestamp timestamp){
return (timestamp == null) ? null : timestamp.toLocalDateTime();
}
하지만 이러한 기능은 다른 곳에서도 쓰일 수 있는 기능이기 때문에 Util 클래스 안에서 static 메소드로 만들어주면 다른 곳에서도 쉽게 해당 기능을 사용할 수 있다.
// LocalDateTime과 관련된 메소드는 아래 Util 클래스에 static 하게 두어 언제든 사용할 수 있도록 한다.
public class LocalTimeUtils {
private LocalTimeUtils() {
}
// Timestamp -> LocalDateTime으로 바꿔주는 static 메소드
public static LocalDateTime getLocalDateTime(Timestamp timestamp){
return (timestamp == null) ? null : timestamp.toLocalDateTime();
}
}
public List<User> findAll() {
...
LocalTimeUtils.getLocalDateTime(rs.getTimestamp("last_login_at"));
...
}
✨ 유틸클래스를 잘 활용하면 코드 재사용성을 높일 수 있을 것같다. 해당 메소드가 다른 클래스에서도 광범위하게 사용될 수 있는가를 잘 판단해야겠다.
6. Mock Test > MockBean Setting
RestController 클래스에 대한 Mock Test 클래스를 만들어주었다.
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = UserRestController.class)
public class UserRestControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
public void testCreateUser() throws Exception {
UserRequestDTO user = new UserRequestDTO("test@test.com", "password!@#");
UserResponseDTO response = new UserResponseDTO(true, "가입성공");
when(userService.createUser(user)).thenReturn(response);
this.mockMvc.perform(MockMvcRequestBuilders
.post("/api/users/join")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
.andExpect(MockMvcResultMatchers.jsonPath("$.response").value("가입성공"))
.andDo(print());
}
....
}
✨ 하지만 RestController 클래스는 UserService에만 의존하고 UserRepository와는 직접적인 의존관계가 아니다. 그렇기 때문에 UserRepository은 @MockBean 설정을 따로 해주지 않아도 된다.
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = UserRestController.class)
public class UserRestControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private UserService userService;
@Test
public void testCreateUser() throws Exception {
...
}
....
}
7. DTO class field should be private
말그대로다! DTO 클래스는 기본적으로 계층간의 데이터 교환에 필요한 클래스다. 교환 과정에서 데이터가 손상되면 안되기 때문에 private으로 설정해준다! DTO 클래스의 setter를 지우라는 코멘트도 받았는데 위와 같은 이유 때문인 것같다.
8. Static Lambda
원래는 이렇게 긴 코드다 허허
아직 람다식이 익숙하지 않았고.. 두 메소드 안에 중복된 뚱뚱한 코드들이 들어가있었다.. 그리고 역시나 이것에 대한 코멘트를 받았다. (rs, rowNum) -> new User.Builder() 부분이 중복되는데 이 부분을 static lambda로 바꿔보는건 어떨까요? 라고
@Override
public User findById(Long id) {
String SELECT_BY_ID_QUERY = "SELECT * FROM users WHERE seq = ?";
List<User> results = this.jdbcTemplate.query(SELECT_BY_ID_QUERY,
new Object[] {id}, (rs, rowNum) -> new User.Builder()
.seq(rs.getLong("seq"))
.email(rs.getString("email"))
.passwd(rs.getString("passwd"))
.loginCount(rs.getInt("login_count"))
.lastLoginAt(LocalTimeUtils.getLocalDateTime(rs.getTimestamp("last_login_at")))
.createAt(rs.getTimestamp("create_at").toLocalDateTime())
.build());
return results.isEmpty() ? null : results.get(0);
}
@Override
public List<User> findAll() {
String SELECT_ALL_QUERY = "SELECT * FROM users";
List<User> results = this.jdbcTemplate.query(SELECT_BY_ID_QUERY,
new Object[] {}, (rs, rowNum) -> new User.Builder()
.seq(rs.getLong("seq"))
.email(rs.getString("email"))
.passwd(rs.getString("passwd"))
.loginCount(rs.getInt("login_count"))
.lastLoginAt(LocalTimeUtils.getLocalDateTime(rs.getTimestamp("last_login_at")))
.createAt(rs.getTimestamp("create_at").toLocalDateTime())
.build());
return results.isEmpty() ? null : results;
}
그래서 나름 static lambda에 대해 요리조리 알아보고 [참조]
적용한 결과다
먼저 User 클래스 안에 있는 static 타입 Builder 클래스 내부에 static 타입의 getEntity() 메소드를 아래와 같이 만들어주었다. (findById 안에 있는 지저분한 코드를 Builder로 데리고 온 것이다)
public static User getEntity(ResultSet rs, int rowNum) throws SQLException {
return new User.Builder()
.seq(rs.getLong("seq"))
.email(rs.getString("email"))
.passwd(rs.getString("passwd"))
.loginCount(rs.getInt("login_count"))
.lastLoginAt(LocalTimeUtils.getLocalDateTime(rs.getTimestamp("last_login_at")))
.createAt(rs.getTimestamp("create_at").toLocalDateTime())
.build();
}
그리고 findById()와 findAll()은 static lambda를 통해서 한 줄로... 줄였다..
@Override
public User findById(Long id) {
String SELECT_BY_ID_QUERY = "SELECT * FROM users WHERE seq = ?";
List<User> results = this.jdbcTemplate.query(SELECT_BY_ID_QUERY, User.Builder::getEntity);
return results.isEmpty() ? null : results.get(0);
}
@Override
public List<User> findAll() {
String SELECT_ALL_QUERY = "SELECT * FROM users";
List<User> results = this.jdbcTemplate.query(SELECT_ALL_QUERY, User.Builder::getEntity);
return results.isEmpty() ? null : results;
}
비록 빌더에게 짐을 지우게 했지만..감동의 쓰나미..😭 이거야 이거
추가 예정
+ Optional 클래스
+ Service 로직과 Respository 로직 분리
'웹' 카테고리의 다른 글
[git] local repository의 remote URL 변경 (0) | 2020.11.21 |
---|---|
[git] merge unrelated project (0) | 2020.11.21 |
[JWT] JWT 구성과 생성 과정 (0) | 2020.11.16 |
[Spring] Spring Boot 프로젝트 고군분투 1주차 (0) | 2020.11.11 |
[DB] SQL 문법 - 기본 함수 (0) | 2020.11.01 |