1. 기본 프로젝트 설정
위의 주소에서 간단한 프로젝트 설정을 진행한다.
Generate 버튼을 클릭해준 후 IntelliJ로 열어주면 아래와 같이 기본 코드 구조가 생성되는 것을 확인할 수 있다.
아래는 기본 build.gradle 내용이다.
spring initializer에서 설정한 부분이 반영된 것을 확인할 수 있고 java는 17로 설정되어 있는 것을 확인할 수 있다.
dependencies에서 보면 spring-boot-starter-thymeleaf와 spring-boot-starter-web이 추가된 것을 확인할 수 있는데 이는 dependencies에서 추가한 내용이다.
thymeleaf의 경우에는 html을 만들기 위한 템플릿 엔진으로 추가하였으며 이러한 dependencies들은 mavenCentral이라는 사이트에서 다운을 받는 것으로 기본 repositories에 mavenCentral이 추가되어 있다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
이후에 실행 버튼을 통해서 실행하면 아래와 같은 로그를 확인할 수 있다.
노란 색 로그를 보면 8080 port에 tomcat이 initialize를 하고 starting service를 한 것을 확인할 수 있는데 프로젝트를 실행하면 자동으로 tomcat까지 설정이 되어 매우 편리하다.
그리고 localhost:8080에 접속하면 Whitelabel Error Page를 확인할 수 있다.
아직 아무 코드도 작성하지 않았기에 해당 페이지가 뜨는 것이 당연하다.
src/main/resources/static 폴더에 index.html을 만들어주면 스프링부트에서 자동으로 Welcome Page 기능을 제공한다.
<!DOCTYPE HTML>
<html>
<head>
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<a href="/hello">hello</a>
</body>
</html>
2. 빌드하고 실행하기
인텔리제이에서 실행하는 것 외에 직접 빌드하고 실행하는 과정이다.
cmd 창에서 ./gradlew build를 입력하면 cd build/libs에 프로젝트-버전-SNAPSHOT.jar 가 생성된다.
생성된 jar 파일을 java -jar 프로젝트.jar로 실행해준다.
인텔리제이에서 실행한 것과 동일하게 서버가 실행되는 것을 확인할 수 있다.
3. 스프링 웹 개발 기초
웹을 개발하는 것의 의미는 3가지로 나누어 볼 수 있다.
1. 정적 컨텐츠
서버에서 보여주는 것이 아닌 파일 그대로를 웹 브라우저 상에서 보여준다.
Spring Boot는 정적 컨텐츠를 기본으로 제공한다. (/static 폴더에서 찾아서 보여준다)
2. MVC와 템플릿 엔진
JSP, PHP와 같은 템플릿 엔진을 통해서 동적으로 보여주기 위해 Model, View, Controller 패턴으로 개발한다.
1번과의 차이는 정적으로 보여주는 것이 아니라 서버에서 변형된 정보를 내려주는 형식이다.
MVC는 Model, View, Controller의 약자로
3. API
JSON 데이터 포맷을 통해 클라이언트에게 데이터를 전달하는 방식이다.
화면은 클라이언트에서 그리고 서버끼리 통신할 때에도 유용하게 사용한다.
★ TIP : Getter/Setter 단축키
IntelliJ에서는 클래스를 만들고 직접 Getter/Setter, 생성자를 생성할 필요 없이 Alt + Insert 키를 눌러서 생성할 수 있다.
@ResponseBody를 사용하고 객체를 반환하면 객체가 JSON 형태로 반환되는 것을 확인할 수 있다.
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
@ResponseBody를 통해서 객체를 인식하고 Json 형태로 데이터를 변환하여 HTTP 응답에 반환하는 것이 정책으로 정해져 있다.
기존에는 ViewResolver가 작동하였는데 @ResponseBody가 있으면 HttpMessageConverter가 동작한다.
문자일 경우 StringConverter가 동작하고 객체일 경우 JsonConverter가 동작하여 형식을 변환해서 return한다.
Jackson과 Gson은 Java에서 JSON 데이터를 처리하기 위해 널리 사용되는 두 가지 라이브러리인데 Spring의 기본은 Jackson이다. java 객체와 json 사이의 변환을 도와준다.
JavaBeans
JavaBeans는 재사용 가능한 소프트웨어 컴포넌트를 만들기 위한 Java의 클래스 규약이다.
아래와 같은 특징을 가지고 있다.
1) 직렬화(Serializable)
객체를 저장하거나 네트워크로 전송하기 위해 사용한다.
2) 기본 생성자
파라미터가 없는 public 생성자를 제공해야 한다.
Bean을 생성하고 설정할 때 필요한 기본 설정을 가능하게 한다.
3) 프로퍼티 접근자 메서드(Getter, Setter)
프로퍼티는 클래스의 멤버 변수로 외부에서 접근할 수 있도록 제공되는 값이다.
Getter, Setter 메서드를 통해서 프로퍼티의 값을 설정하고 반환한다.
4) 캡슐화
프로퍼티는 private 접근 제한자를 사용해서 외부에서 직접 접근하지 못하게 한다.
4. 백엔드 개발 (회원 관리 예제)
1) 비즈니스 요구사항 정리
컨트롤러 : 웹 MVC의 컨트롤러 역할
서비스 : 핵심 비즈니스 로직 구현
리포지토리 : 데이터베이스에 접근하고 도메인 객체를 DB에 저장하고 관리하는 역할
도메인 : 비즈니스 도메인 객체, 데이터베이스에 저장되고 관리되는 객체
2) 회원 domain과 repository 만들기
project path는 생략하겠습니다(hello.hello_spring)
/src/main/java/project/domain/Member.java 파일을 생성한다.
package hello.hello_spring.domain;
public class Member {
private Long 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;
}
}
기본적으로 id와 name만 가지고 있는 Member 클래스다.
/src/main/java/project/repository/MemberRepository
DB가 어떤 것인지 결정되지 않은 상황이므로 interface로 MemberRepository를 생성한다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
/src/main/java/project/repository/MemoryMemberRepository
아래는 위의 Repository를 implements한 MemoryMemberRepository이다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository {
// 실무에서는 concurrentHashMap을 쓰는게 좋음
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
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();
}
}
store의 경우 동시성을 위해서 concurrentHashMap을 쓰는 게 좋다.
3) 회원 repository 테스트 케이스 작성
생성한 MemoryMemberRepository를 테스트하기 위해서 /test/java/project/repository/MemoryMemberRepositoryTest를 생성한다.
package hello.hello_spring.repository;
import hello.hello_spring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
//given
Member member = new Member();
member.setName("spring");
//when
repository.save(member);
//then
Member result = repository.findById(member.getId()).get();
assertThat(result).isEqualTo(member);
}
@Test
public void findByName() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
Member result = repository.findByName("spring1").get();
//then
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
//given
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
//when
List<Member> result = repository.findAll();
//then
assertThat(result.size()).isEqualTo(2);
}
}
given -> when -> then 으로 테스트 코드를 작성하자.
주어진 값이 어떤 상황일 때 결과값을 갖는지를 테스트하는 식으로 작성하자.
@AfterEach를 사용해서 repository.clearStore를 하는데 테스트 코드를 실행할 때 실행 순서의 보장이 없기 때문에 각 테스트가 끝날 때마다 repository를 clear하여야 서로 영향을 미치지 않고 작동한다.
4) 회원 서비스 개발
회원 리포지토리와 도메인을 활용해서 실제 서비스를 구현하는 부분이다.
★ TIP : 자동 return 완성 커맨드
IntelliJ에서 ctrl + alt + v 키를 사용하면 자동으로 가져오는 값의 return 값을 설정하여준다.
// 이전 코드
memberRepository.findByName(member.getName());
// ctrl + alt + v
Optional<Member> byName = memberRepository.findByName(member.getName());
★ TIP : 리팩터링
IntelliJ에서 ctrl + alt + shift + T 키를 사용하면 리팩터링을 쉽게 할 수 있다.
메서드 추출이나 변수명 변경 등 다양한 리팩토링을 쉽게 할 수 있다.
서비스 코드는 아래와 같다.
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository;
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);
}
}
회원 가입하는 join 메서드, 전체 멤버를 확인하는 findMembers 메서드, id로 멤버를 조회하는 findOne 메서드를 작성하였다.
★ TIP : 의존성 주입
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
기존에는 memberRepository = new MemberRepository()로 생성하였는데 이럴 경우 테스트하는 Repository와 서비스에서 사용하는 Repository가 다른 문제가 발생한다.
이를 해결하기 위해서 new로 생성하는 것이 아닌 생성자를 통해서 외부에서 직접 넣어주도록 한다.
정리하자면 서비스에서 사용하는 Repository는 서비스마다 직접 또 생성하는 것이 아니라 사용하는 Repository를 주입하여서 그대로 사용하도록 DI를 적용시키는 것이다.
5) 회원 서비스 테스트
회원 서비스 테스트 코드는 아래와 같다.
BeforeEach에서는 의존성을 주입하여 Repository를 설정해주는 것을 확인할 수 있다.
package hello.hello_spring.service;
import hello.hello_spring.domain.Member;
import hello.hello_spring.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.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
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 = memberRepository.findById(saveId).get();
assertEquals(member.getName(), 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("이미 존재하는 회원입니다.");
}
}
'dev > 기본쌓기' 카테고리의 다른 글
[JAVA] 빠르게 정리하는 자바 문법 (3) (4) | 2024.12.30 |
---|---|
[JAVA] 빠르게 정리하는 자바 문법 (2) (4) | 2024.12.30 |
[JAVA] 빠르게 정리하는 자바 문법 (1) (2) | 2024.12.30 |
[스프링 입문] Spring Boot 입문(3) (6) | 2024.12.18 |
[스프링 입문] Spring Boot 입문(2) (0) | 2024.12.11 |