회원가입을 선택해서 회원가입 페이지로 넘어가 회원등록을 하고, 회원목록에 들어가면 동록된 회원의 email(id)을 확인할 수 있는 프로젝트이다.
이를 구현하는 과정에서 spring mvc, service와 transaction,jpa repository, builder패턴, entity의 설계 등을 점검하려 한다. 로그인 기능은 아직 구현하지 않았다.
위와 같은 구조에서 respoitory에 대한 설정을 java config로 해보고, 리포지토리와 서비스를 DI를 생각하며 구현해 보도록할 것이다.
boot strap을 이용했다. bootstrap example의 css까지 static에 만들어주어야 스타일이 스타일이 적용되어 보여진다. 크롬에서 F12를 눌러 개발자 도구를 들어가면 css파일이 제대로 로드 되었는지 확인할 수 있다.
<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>Home</title>
<link rel="canonical" href="https://getbootstrap.com/docs/5.1/examples/sign-in/">
<!-- Bootstrap core CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<!-- Favicons -->
<link rel="apple-touch-icon" href="/docs/5.1/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/docs/5.1/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/docs/5.1/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="/docs/5.1/assets/img/favicons/manifest.json">
<link rel="mask-icon" href="/docs/5.1/assets/img/favicons/safari-pinned-tab.svg" color="#7952b3">
<link rel="icon" href="/docs/5.1/assets/img/favicons/favicon.ico">
<meta name="theme-color" content="#7952b3">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="signin.css" rel="stylesheet">
</head>
<body class="text-center">
<main class="form-signin">
<form th:action="@{/home}" method="post">
<img class="mb-4" src="member-logo2.svg" alt="" width="72" height="57">
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
<div class="form-floating">
<input type="text" class="form-control" name="email" id="email" placeholder="name@example.com">
<label for="email">Email address</label>
</div>
<div class="form-floating">
<input type="password" class="form-control"
name="password" id="password" placeholder="Password">
<label for="password">Password</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
<a href="/users/new">회원가입</a>
<a href="/users/list">회원목록</a>
<p class="mt-5 mb-3 text-muted">© 2021-01</p>
</form>
</main>
</body>
</html>
회원을 DB에 저장할 Entity를 구현하겠다.
@Getter
@NoArgsConstructor
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String password;
private String email;
@Builder
public User(Long id,String password,String email){
this.id=id;
this.password=password;
this.email=email;
}
}
Entity 설계 과정에서 setter를 남발하지 않아야 한다는 것을 알았다. setter가 선언되어 있으면 다른 객체에서 entity객체의 값을 쉽게 바꿀 수 있어 관리가 어렵다. 때문에 getter만 구현하고 값을 바꿔야 하는 경우에는 값이 바뀐다는 것을 명확히 알 수 있는 메소드 (ex: deleteUser(),updateUser...) 안에서 생성자나 builder를 이용하는 방식이 확장성의 면에서 좋다.
spring data jpa를 이용하면 repository를 implement해 구현하지 않아도 sql처리가 가능하다. 물론 복잡하고 세밀한 쿼리가 필요할 때는 sql문을 입력해야 할 경우도 있지만 큰 흐름에 집중하기 위해 간단하게 JpaRepository를 extends해 구현하겠다.
public interface MemberRepository {
User save(User user);
Optional<User> findByEmail(String email);
List<User> findAll();
}
public interface UserRepository extends JpaRepository<User,Long>,MemberRepository {
}
+참고: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
놀랍게도 형식에 맞게 메소드 이름을 정하면 jpql로 알아서 변환해 구현하지 않아도 db접근이 가능하다.
Userservice는 UserRepository에 의존한다. 하지만 의존하는 클래스가 new JpaRepoistory()와 같은 구현체를 알아야 한다는 것은 객체지향의 장점이 잘 발휘되지 못한 것이다. 위처럼 spring sata jpa를 사용할 경우 repository의 구현체가 없을 수 있어 실감이 안 날 수 있지만, java config을 이용해서 repository의 구현체가 바뀌더라도 Service나 controller를 수정할 필요가 없도록 해보자.
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public UserService userService() {
return new UserService(memberRepository);
}
}
Controller에서 인자없는 생성자나 @RequiredArgsConstructor 같은 어노테이션을 이용해 UserService의 구현체를 모른채로 생성하기만 하면 spring config에서 설정한 userRepository가 주입된 생성자가 실행된다. 만약 Repository의 구현체가 바뀐다면 SpringConfig에서 new UserService()의 인자만 바꾸면 된다.
public class UserService {
private final MemberRepository memberRepository;
public UserService(MemberRepository memberRepository){
this.memberRepository=memberRepository;
}
public void join(User user){
validateDuplicateMember(user);
memberRepository.save(user);
}
private void validateDuplicateMember(User user) {
memberRepository.findByEmail(user.getEmail())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
public List<User> findAll(){
return memberRepository.findAll();
}
}
service에서 repository에서 설계한 메소드들을 이용해 로직을 수행한다. 이때 java config로 @Bean 등록을 해놓았으므로 @Service어노테이션 사용하면 이미 생성된 bean이 있으므로 에러가 발생한다. 컴포넌트 스캔을 이용한 방법, 직접 @Bean으로 등록하는 방법 둘 중 하나만 사용하도록 하자.
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
@GetMapping("users/list")
public String list(Model model){
model.addAttribute("user",userService.findAll());
return "users/list";
}
@GetMapping("users/new")
public String createUserForm(){
return "users/createuser";
}
@PostMapping("users/new")
public String createUser(UserForm form){
User user= User.builder().email(form.getEmail()).password(form.getPassword()).build();
userService.join(user);
return "redirect:/home";
}
}
여기서 특징은 @requiredargsconstructor 어노테이션이 붙어있다는 것이다. bean을 주입 받는 방식은 1. 생성자 2. setter 3.@Autowired 가 있는데 여기서는 생성자를 이용해 bean을 주입했다. requiredargsconstructor를 이용한 방법의 장점은 의존성을 쉽게 바꿀 수 있다는 점이다. service가 예를 들어 KoreaUserService로 바뀌더라도 생성자를 호출할 필요없이 final로 선언만 하면 된다.
그리고 builder패턴을 사용했다. 생성자로 객체를 생성할 경우보다 변수와 인자의 매칭을 더 명확하게 볼 수 있다.
- Entity에는 불필요하게 setter를 생성하지 않는다.
- Entity의 변화를 일으키는 메소드에는 @transactional을 붙인다.
- Entity를 Request/Response 클래스로 사용하지 않는다. 직접 입력받지 않고 dto를 이용한다. Entity가 프론트의 변화사항에 따라 종속적으로 바뀌는 것은 좋지않다.
- 의존성 주입은 생성자,필드주입(@Autowired),수정자(setter) 3가지 방법이 있지만 가능하면 생성자를 이용하자. 순환참조를 예방하고 테스트 코드를 짜기 더 용이하다.
+참고: https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection- 의존성 주입방식을 java config에서 등록하는 방식 외에도 final로 선언하고 @RequiredArgsConstructor 어노테이션을 붙여 쉽게 의존성을 변경할 수 있게 할수 있다. 정형적이지 않거나 특징적으로 알아야 할 설정이 있을 경우에 java config에 따로 알 수 있게 작성하는 것이 나을 듯 하다.
+참고도서: 스프링부트와 aws로 혼자 구현하는 웹서비스