일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- MVC
- Java
- Uipath
- React
- mysql
- db
- 이클립스
- html
- 문자열
- spring
- jquery
- rpa
- Eclipse
- jsp
- SpringBoot
- 조건문
- 상속
- git
- Thymeleaf
- string
- View
- Array
- Oracle
- Database
- Controller
- JDBC
- 배열
- Board
- Scanner
- API
- Today
- Total
유정잉
🎀 스프링 환경 설정, 간단한 회원가입 빌드 작성 본문
[ spring project 생성 ]
1. start.spring.io 사이트에서 project 생성
Gradle - Groovy 선택 버전은 3.0 이상 사용. Group은 package명 Arifact는 project명
Dependencies는 내가 사용할 라이브러리 선택후 GENERATE
2. 1번에서 생성한 project 파일 압축 풀기 후 -> build.gradle 선택 후 Open
3. 요즘에는 project 선택하면 main test 폴더가 자동으로 나눠져서 생성 됨
test코드가 요즘 개발 트렌드에서 중요함 !!
resource에는 실제 자바 코드파일을 제외한 설정파일이나 xml이나 properties가 들어가는 폴더 (=즉, 자바파일제외한 나머지)
build.gradle은 start.spring.io에서 자동으로 제공 된 설정 파일
gitignore는 소스코드 관리해주는 파일
4. 라이브러리 의존성 확인
[ View 환경설정 ]
1. main/resources/static -> index.html 생성
2. webserver가 파일을 그대로 던져주는 것이 아닌 탬플릿엔진을 사용하면 내가 원하는대로 루프를 넣거나 모양을 바꿀 수 있음
=> 타임리프 사용
3. resource/templates 폴더에 있는 html 파일을 찾아서 타임리프 템플릿 엔진 처리
4. 타임리프 선언 (.html에서)
<html xmlns:th="http://www.thymeleaf.org">
[ 빌드하고 실행하기 ]
콘솔로 이동
1. `./gradlew build`
2. `cd build/libs`
3. java -jar hello-spring-0.0.1-SNAPSHOT.jar
4. 실행확인
[ 회원관리예제 만들기 ]
1. 비즈니스 요구사항 정리
2. 회원 도메인과 리포지토리 만들기
3. 회원 리포지토리 테스트 케이스 작성
4. 회원 서비스 개발
5. 회원 서비스 테스트
1) 비즈니스 요구사항 정리
데이터 : 회원 ID, 이름
기능 : 회원 등록, 조회
DB는 아직 정해지지 않음
2) domain 패키지에 Member 클래스 생성
package hello.hellospring.domain;
public class Member {
private Long id; //시스템이 저장한느 id(고객이 정하는 id가 아님)
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3) repository 패키지 만들기 (=회원 객체를 저장하는 저장소) -> Interface로 MemberRepository 만들기
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member); //회원을 저장하면 저장된 회원이 반환
Optional<Member> findById(Long id); //Id로 회원을 찾기
Optional<Member> findByName(String name); //Name으로 회원 찾기
List<Member> findAll(); //지금까지 저장된 모든 회원 리스트 반환
}
4) repository 패키지에 MemoryMemberRepository class 생성 후 implements MemberRepository -> implements methods
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
//implements MemberRepository 후 🩷 optoion+Enter=Implement methods 생성 됨
@Repository
public class MemoryMemberRepository implements MemberRepository{
// 회원의 id=Long, 값은 Member save할때 저장될 객체를 Map으로 설정
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L; //sequence는 0,1,2 키값을 생성해주는 역할
@Override
public Member save(Member member) {
member.setId(++sequence); //member를 save할때 sequence값 하나 올려줌
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //null이 반환될 가능성이 있으서 Optional로 감싼다
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream().filter(member -> member.getName().equals(name)).findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
5) test에 main에서 만든거랑 똑같이 패키지랑 클래스 생성 이름뒤에만 Test붙이기 MemoryMemberRepositoryTest
여기서는 MemoryMemberRepository에서 만든 save가 잘 동작하는지 Test 해보는 것 !
메서드에 @Test 어노테이션 붙여주기만 하면 됨 !
Test는 순서랑 상관 없이 메서드별로 따로 실행 됨 ! 순서는 알아서 지켜지지 않음 ! -> @AfterEach clearStore 필요
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore(); //Test가 실행되고 끝날때마다 저장소를 비움 !
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get(); //repository.save에 제대로 값 들어갔나 검증
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get(); //만약에 "spring2"로 바꾸면 오류 !
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
6) service 패키지 생성 MemberService class 생성
ifPresent를 사용하면 이미 존재하는 값이라면 "이미 존재하는 회원입니다" 출력 됨 (=null이 아니면)
🩷 control + T = Refactor -> Extract Method
//회원가입 + 같은 이름 중복회원 불가능 로직
public long join(Member member) {
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
//회원가입 + 같은 이름 중복회원 불가능 로직
public long join(Member member) {
//Optional 생략하고 더 간단하게 코드 작성 가능 -> control + T 로 더 간결하게 가능
memberRepository.findByName(member.getName()).ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
memberRepository.save(member);
return member.getId();
}
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class MemberService {
//회원 서비스를 만드려면 회원 리포지토리가 필요
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
//회원가입 + 같은 이름 중복회원 불가능 로직
public long join(Member member) {
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName()).ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
//전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
7) 회원 서비스 테스트 MemberServiceTest
🩷 command + shift + T = 자동 @Test 생성
given 무언가가 주어졌는데 / when 이거를 실행했을 때 / then 결과가 이게 나와야 함 으로 나눠서 생성 (given/when/then)
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
// 🩷 command + shift + T = 자동 @Test 생성
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach //돌때마다 끝나고나면 DB의 값을 날려줌
public void afterEach() {
memberRepository.clearStore();
}
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));//예외가 발생해야 한다. assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
}
}
8) MemberController class 생성
@Component 컴포넌트 스캔과 자동 의존관계 설정 (@Service, @Repository 사용)
package hello.hellospring.controller;
import hello.hellospring.domain.Member;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
9) config 패키지에 SpringConfig class 생성 -> 자바 코드로 직접 스프링 빈 등록 (@Configuration을 사용하여 직접 @Bean 등록)
MemberController에 @Controller만 냅두고 @Service랑, @Repository 지우기 !
package hello.hellospring;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository()); //🩷 command + P = 생성자 뭘 넣어줘야 하는지 보여줌
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
10) 이제부터 WEB 화면 웹 MVC 개발 -> HomeController 클래스 생성 -> home.html 생성
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<h1>Hello Spring</h1> <p>회원 기능</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a> </p>
</div>
</div> <!-- /container -->
</body>
</html>
11) MemberController에 members/new 와 members Mapping 해주기 ->
template 패키지에 members패키지 생성 -> createMemberForm.html 생성
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/"; //home화면으로 보내는 것
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<form action="/members/new" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" name="name" placeholder="이름을 입력하세요">
</div>
<button type="submit">등록</button> </form>
</div> <!-- /container -->
</body>
</html>
12) MemberForm 클래스 생성 후 String name 필드 선언 & GetterSetter 생성
MemberController에 members/new 와 members Mapping 해주기 ->
template 패키지에 members패키지 생성 -> memberList.html 생성
package hello.hellospring.controller;
public class MemberForm {
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<!-- 타임리프로 로직을 돌면서 이 부분이 반복됨 -->
<tr th:each="member : ${members}">
<!-- 첫번째 값을 담고 출력하고 ~ 반복으로 랜더링 됨 -->
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
@Controller 어노테이션 설정을 하면 스프링 컨테이너에서 스프링 빈이 관리된다 !
스프링 컨테이너에 등록을 하게 되면 객체를 필요할때마다 new로 생성할 필요 없이 하나만 생성해서 여러군데서 사용할수 있음
즉, private final MemberService memberService = new MemberService(); 대신
private final MemberService memberService; 사용
같은 스프링빈이면 보통 같은 인스턴스 (=싱글톤), 물론 다르게 설정할 수도 있긴 함
[ 스프링 빈을 등록하는 2가지 방법 ]
1. @Component 컴포넌트 스캔과 자동 의존관계 설정 (@Service, @Repository 사용)
2. 자바 코드로 직접 스프링 빈 등록 (..Config에 @Configuration을 사용하여 직접 @Bean 등록)
두가지 방법에 각각 장단점 존재 !
: 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다.
그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
`@Autowired를 통한 DI는 helloController, memberService등과 같이 스프링이 관리하는 객체에서만 동작한다.
스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
[ DI 주입 ]
필드주입 : 별로 안 좋은 방식
@Autowired private MemberService memberService;
setter주입 : public이라는 단점 노출이 됨 ! 아무 개발자나 호출할 수 있게 열려 있어서 안 좋음 !
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
생성자 주입 : 제일 좋은 방법 !!! 의존관계가 실행 중에 동적으로 변하는 경우는 거의 아예 없으므로 생성자 주입 권장 !
'개발자 공부 > 🎀 스프링 공부' 카테고리의 다른 글
🎀 스프링 기본 1 (0) | 2024.05.23 |
---|---|
🎀 AOP (0) | 2024.05.22 |
🎀 JDBC 연결 [ JPA ] (0) | 2024.05.21 |
🎀 H2 데이터베이스 설치 및 실행 (0) | 2024.05.21 |
🎀 MVC 공부 정리 🎀 (0) | 2024.05.18 |