섹션 1: 프로젝트 환경설정
1. 핵심 라이브러리
Gradle은 의존관계가 있는 라이브러리를 함께 다운로드 함.
- 스프링 부트 라이브러리
- spring-boot-starter-webspring-webmvc : 스프링 웹 MVC
- spring-boot-starter-tomcat : 톰캣 (웹서버)
- spring-boot-starter-thymeleaf : 타임리프 템플릿 엔진(View)
- spring-boot-starter(공통) : 스프링부트 + 스프링 코어 + 로깅spring-boot-starter-logging > logback/slf4j
- —> 위치 : [Gradle > Dependencies > complileClasspath >> ch.qos.logback:logback-classisc:1.4.11 / org.slf4j:slf4j-api:2.0.9]
- spring-boot > spring-core
- 테스트 라이브러리
- spring-boot-starter-test—> 위치 : [Gradle > Dependencies > testComplileClasspath >> org.junit.jupiter:junit-jupiter:5.9.3 (5버전을 요즘에는 많이 씀)]assertj : 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
- spring-test : 스프링 통합 테스트 지원
- mockito : 목 라이브러리
- junit : 테스트 프레임워크
2. View 환경설정
- return: hello —> resources > templates > hello.html (resources:templates/ 밑에 있는 파일을 찾아서 랜더링 함)
- 컨트롤러에서 리턴 값으로 문자를 반환하면 ‘viewResolver’가 화면을 찾아서 처리함‘resources:templates/’ + {ViewName} + ‘.html’ —> ex) resources:templates/hello.html
- 스프링 부트 템플릿엔진 기본 viewName 매핑
3. 빌드하고 실행하기
cmd 실행
D:
cd D:\study\hello-spring\hello-spring
gradlew
gradlew build
cd D:\study\hello-spring\hello-spring\build\libs
dir
java -jar hello-spring-0.0.1-SNAPSHOT.jar
서버 배포할 때는 hello-spring-0.0.1-SNAPSHOT.jar 파일 복사해서 서버에 넣고 java -jar.exe 실행
→ 서버에서도 스프링이 동작하게 됨
섹션 2: 스프링 웹 개발 기초
- 정적 컨텐츠 : 파일을 그대로 웹 브라우저에 내려줌
- MVC : 모델, 뷰, 컨트롤러 (컨트롤러 : 모델, 템플릿 엔진, 화면)
- 템플릿 엔진 : 서버에서 프로그래밍해서 HTML을 동적으로 바꿔서 내리는 것
- API : JSON이라는 데이터 포맷으로 클라이언트한테 데이터를 전달하는 방식(요즘 API 방식)
1. 정적 컨텐츠
① 웹 브라우저에서 localhost:8080/hello-static.html → 내장 톰캣 서버
② 내장 톰캣 서버 → 스프링
③ 먼저 컨트롤러 쪽에서 hello-static 이 있는지 찾아봄(컨트롤러가 우선순위를 가짐)
④ hello-static이라는 컨트롤러 없음 → resources:static/hello-static.html을 찾음
⑤ 있으면 반환
2. MVC와 템플릿 엔진
MVC : Model, View, Controller
// HelloController
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam(value = "name") String name, Model model){
model.addAttribute("name", name);
return "hello-template";
}
- @RequestParam(value = "name") String name: 이 어노테이션은 요청 URL에서 "name"이라는 쿼리 매개변수의 값을 추출하여 name 변수에 바인딩합니다.
- Model model: Model 객체는 Spring에서 제공되며, 컨트롤러에서 뷰로 데이터를 전달하는 데 사용됩니다.
- model.addAttribute("name", name): 이 라인은 "name"이라는 속성을 모델에 추가하고 해당 값으로 요청에서 받은 name 매개변수를 설정합니다.
① localhost:8080/hello-mvc?name=spring! 했을 때 name = spring!
② 컨트롤러에서, String name 에서 name → spring! 으로 바뀜
③ model.addAttribute(attributeName: “name”, name); 에서 name → spring!으로 바뀐 후 모델에 담김
④ return “hello-template”에서 hello-template으로 넘어감
// hello-template.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p> // hello! empty?
</body>
⑤ ${name} 에서 name → spring!으로 바뀜
(이때 달러 표시($)는 모델에서 값을 꺼내는 것. 모델의 키값이 name인 것에서 값을 꺼냄)
+) thymeleaf 템플릿의 장점 : HTML을 그대로 쓰고 그 파일을 서버 없이 바로 열어볼 수 있음
( 방법 : 코드 copy > copy path > Absolute Path )
이때 템플릿 엔진으로 동작을 하면 hello empty 위치에 있는 값으로 내용이 치환되어 보여짐
3. API
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name){
return "hello " + name;
}
- @ResponseBody : http의 응답 body에 이 데이터를 직접 넣어주겠다는 의미
- 뷰 없이 이 문자가 그대로 내려감, 페이지 소스 보기에도 코드 없음
최근에는 거의 JSON 방식
+) JSON : 일반적으로 서버에서 클라이언트로 데이터를 보낼 때 사용하는 양식. 클라이언트가 사용하는 언어에 관계 없이 통일된 데이터를 주고받을 수 있도록, 일정한 패턴을 지닌 문자열을 생성해 내보내면 클라이언트는 그를 해석해 데이터를 자기만의 방식으로 온전히 저장, 표시할 수 있게 된다.
- JSON 예제 : 한 사람에 관한 정보를 갖는 JSON 객체
키-값 쌍(이름:값)의 패턴으로 표현된다.
{
"이름": "홍길동",
"나이": 55,
"성별": "남",
"주소": "서울특별시 양천구 목동",
"특기": ["검술", "코딩"],
"가족관계": {"#": 2, "아버지": "홍판서", "어머니": "춘섬"},
"회사": "경기 수원시 팔달구 우만동"
}
+) Getter Setter 단축기 : Alt + Insert
섹션 3: 회원 관리 예제 - 백엔드 개발
1. 비즈니스 요구사항 정리
- 데이터 : 회원id, 이름
- 기능 : 회원 등록하기, 회원 조회하기
- 아직 데이터 저장소가 선정되지 않았다고 가정함.
- 컨트롤러 : 웹 MVC의 컨트롤러 역할
- 서비스 : 핵심 비즈니스 로직 구현
- 도메인 : 비즈니스 도메인 객체(예: 회원, 주문, 쿠폰 등 데이터베이스에 주로 저장하고 관리됨)
- 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리함
- 클래스 의존 관계
아직 데이터 저장소 선정 안됨 → 인터페이스로 구현 클래스 변경 가능하도록 설계(MemberRepository)
초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용(Memory MemberRepository)
2. 회원 도메인과 리포지토리 만들기
① 회원 도메인
main/java/hello.hellospring/domain/Member.java
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 시스템에 저장을 할 때 시스템이 정해주는 id
private String name; // 고객이 회원가입할 때 적는 이름
- id : 시스템에 저장할 때 시스템이 정해주는 id
- name : 고객이 회원가입할 때 적는 이름
② 회원 리포지토리
main/java/hello.hellospring/repository/MemberRepository.java
public interface MemberRepository {
// save : 회원이 저장소에 저장됨
Member save(Member member);
// findById, findByName 으로 저장소에서 찾아올 수 있음
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
// findAll : 지금까지 저장된 모든 회원 리스트를 다 반환해줌
List<Member> findAll();
}
- save : 회원이 저장소에 저장됨
- findById, findByName : 저장소에서 찾아올 수 있음
- findAll : 지금까지 저장된 모든 회원 리스트를 다 반환해줌
main/java/hello.hellospring/repository/MemoryMemberRepository.java
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence); // id setting
store.put(member.getId(), member); // 시스템에 저장 → map에 저장됨
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@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();
}
}
- sequence : 0, 1, 2 등 키값을 생성해줌
- 실무에서는 Long 보다는 동시성 문제를 고려해서 AtomicLong 등으로 함
- null이 반환될 가능성이 있으면 Optional로 감쌈.
- Optional.ofNullable : null이어도 가능해짐
save
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence); // id setting
store.put(member.getId(), member); // 시스템에 저장 → map에 저장됨
return member;
}
- 'store'는 'Member' 객체를 저장하는 데 사용되는 HashMap 타입의 맵이다. 이 맵은 'Long' 형식의 키(여기서는 ID)와 'Member' 객체를 값으로 가진다.
- 'sequence'는 'Member' 객체의 고유한 ID를 생성하는 데 사용되는 변수. 각 save 호출 시마다 이 값이 증가하여 고유한 ID 생성한다.
- memser.setId(++sequence) : Member 객체의 ID 설정, sequence 값을 1씩 증가시키면서 고유한 ID 생성
- store.put(member.getId(), member) : Member 객체를 store 맵에 저장한다. ID가 키로 사용되며, 해당 ID에 대응하는 Member 객체가 값으로 저장된다.
- return member : 메서드에 저장된 Member 객체를 반환한다.
findById
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
- id를 사용하여 store 맵에서 해당하는 멤버를 찾아 Optional로 감싸서 반환한다.
- Optional.ofNullable은 null인 경우 빈 Optional을 반환하며, 그렇지 않은 경우 해당 멤버를 감싸서 반환한다.
findByName
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
- name을 사용하여 store 맵에서 해당하는 멤버를 찾아 Optional로 감싸서 반환한다.
- store.values() 는 store 맵에서 값들만 가져온다. 여기서 값은 Member 객체들이다.
- stream()을 사용하여 맵의 값들을 스트림으로 변환한다. 스트림은 데이터를 처리하기 위한 일련의 연속적인 작업을 지원하는 Java 8 이후의 기능이다.
- filter를 통해 주어진 이름과 일치하는 멤버를 찾는다.
- findAny 는 찾은 멤버 중 아무거나 하나를 반환한다.
findAll
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
- 모든 멤버를 반환한다.
- new ArrayList<>(...) : store.values()에서 반환된 값들을 새로운 ArrayList에 담아준다
- 최종적으로 새로 생성된 ArrayList를 반환한다.
3. 회원 리포지토리 테스트 케이스 작성
JUnit 프레임워크로 테스트 코드를 만들고 테스트 코드 자체를 실행
test/java/hello.hellospring/repository/MemoryMemberRepositoryTest
save 테스트
@Test
public void save(){
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
// Assertions.assertEquals(member, result);
assertThat(member).isEqualTo(result);
}
- member 생성
- 이름 spring으로 설정
- repository에 member 저장
- repository에서 member의 id와 같은 멤버를 찾아 꺼내기
- Assertions.assertEquals(expected, actual) : member(expected)와 result(actual)가 같은지
- Assertions.assertThat(member).isEqualsTo(result) 로 많이 씀
- 이때 Assertions에서 Alt+Enter → Add on-demand static import for ~~ 클릭
→ import static org.assertj.core.api.Assertions.*; 추가되고, assertThat(member).isEqualTo(result)로 간단하게 입력 가능
findByName
@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();
assertThat(result).isEqualTo(member1);
}
- member1, member2 생성
- member1 이름 : spring1, member2 이름 : spring2
- repository에서 spring1 이름을 가진 멤버 꺼내기
- result와 member1이 같은지 확인
findAll
@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);
}
- repository에 member1(spring1), member2(spring2) 2명 저장
- repository의 모든 멤버 꺼내기
- result.size()가 2 인지 확인
error!
- save, findByName, findAll 을 모두 실행하면 에러가 뜸
- 실행순서 : findAll() → findByName() → save() 실행순서는 보장되지 않음
why?
findAll()이 실행될 때 spring1, spring2가 저장됨
그래서 findByName()이 실행될 때 이전에 저장했던 spring1이 나와버림
해결방법?
테스트가 하나 끝나고 나면 이 데이터를 클리어 해줘야 함.
→ 테스트 끝날 때마다 repository 를 깔끔하게 지워주는 코드 추가
@AfterEach
public void afterEach(){
repository.clearStore();
}
public void clearStore(){
store.clear();
}
>> 테스트가 실행이 되고 끝날 때마다 한 번씩 저장소를 다 지움 → 순서와 상관 없어짐 → 해결
※ 테스트는 서로 그 순서와 관계없이 서로 의존관계 없이 설계가 되어야 함.
+) TDD : 테스트 주도 개발
+) Quick Fix, show intention actions 단축키 : Alt + Enter
Rename 단축키 : Shift + F6
3. 회원 서비스 개발
main/java/hello.hellospring/service/MemberService.java
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
/* 회원 가입 */
public Long join(Member member){
// 같은 이름이 있는 중복 회원은 가입 X
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);
}
}
+) 변수 추출하기 단축키 : Ctrl + Alt + V
refactor this 단축키 : Ctrl + Alt + Shift + T
4. 회원 서비스 테스트
test/java/hello.hellospring/repository/MemberServiceTest.java
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("hello");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외(){
// 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));
// then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
- afterEach() : 각 테스트 끝나면 clearStore()
중복 회원 예외
@Test
public void 중복_회원_예외(){
// 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));
// then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
- given
: 두 개의 Member 객체를 생성하고 각각에 동일한 이름("spring")을 설정한다
- when
memberServcie.join(member1) 을 호출하여 첫 번째 회원 가입시킨다.
memberService.join(member2) 을 호출하면 예외가 발생해야 한다.
- then
assertThrows 를 사용하여 IllegalStateException 이 발생하는지 확인한다
발생한 예외의 메시지가 "이미 존재하는 회원입니다." 와 일치하는지 확인한다
섹션 4: 스프링 빈과 의존관계
스프링 빈을 등록하는 2가지 방법
① 컴포넌트 스캔과 자동 의존관계 설정
② 자바 코드로 직접 스프링 빈 등록하기
1. 컴포넌트 스캔과 자동 의존관계 설정
회원 컨트롤러가 회원 서비스와 회원 리포지토리를 사용할 수 있게 의존관계 준비
회원 컨트롤러에 의존관계 추가
main/java/hello.hello-spring/controller/MemberController.java
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
- @Controller 가 있으면 자동으로 스프링 빈으로 등록
- 생성자에 @Autowired가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다
→ 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI(의존성 주입) 이라고 한다.
- 컴포넌트 스캔 원리
@Component 애노테이션이 있으면 스프링 빈으로 자동 등록됨
→ @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문.
- @Component를 포함하는 애노테이션
@Controller
@Service
@Repository
→ 스프링 빈으로 자동 등록됨
// 회원 서비스 스프링 빈 등록
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
- 생성자에 @Autowired를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입함
- 생성자가 1개만 있으면 @Autowired는 생략 가능
// 회원 리포지토리 스프링 빈 등록
@Repository
public class MemoryMemberRepository implements MemberRepository {}
→ memberService, memberRepository가 스프링 컨테이너에 스프링 빈으로 등록됨
2. 자바 코드로 직접 스프링 빈 등록하기
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
'Backend > 인프런 강의: 김영한 스프링' 카테고리의 다른 글
| 스프링 입문: 섹션 5부터 7까지 (1) | 2023.11.17 |
|---|