스프링부트 / JPA / Gradle / HIBERNATE / Thymeleaf
스프링 환경설정: https://start.spring.io
Dependencies 추가하기
1. Spring Web (모듈) : MVC / 내장 웹 서버 / viewResolver, 핸들러 매핑 등의 기본적인 웹 애플리케이션 구축 / 정적 리소스 (이미지, CSS등) 처리 / RESTful 웹 서비스 (엔드 포인트 지정용 어노테이션...)
2. Thymleaf (템플릿 엔진)
resources : 정적 데이터 (이미지, css, 설정파일)
build.gradle : 버전 설정 / dependencies (라이브러리 추가) / mavenCentral -> 라이브러리를 다운 받아오는 곳 / gitignore -> 자동으로 해줌
Spring Web 라이브러리를 가져오면, spring-boot-starter-web이 의존관계를 맺는 모든 라이브러리들을 가져온다.
톰캣 - 임베디드( 내장 ) 서버
Spring Core : 스프링의 근간이 되는 요소 IoC / DI 기능 지원
Spring - log : println을 통해 출력 x -> log 파일로 에러 찾기 slf4j / logback 찾아보기
웰컴페이지 만들기
resources/static/index.html 만들기

resources:templates/ + {ViewName} + .html인텔리제이 안에서 실행한 것과 달리, 명령줄을 통해 빌드하기
- 해당 프로젝트 dir로 이동후, build
./gradlew build (Linux)
gradlew.bat build (cmd)
java -jar xxxxxxx.jar
./gradlew clean
html 파일을 서버에 그대로 내려주는 형태
가장 많은 방식 / JSP 혹은 PHP와 같이 템플릿 엔진을 통해 서버에서 프로그래밍을 한 뒤, html을 동적으로 바꿔서 내려주는 형태 (Model - View - Controller)
cf) Model1 방식 -> View에서 모든 것을 처리 (코드 가독성 하향)
- Controller : 데이터 처리, 비지니스 로직 관리에 집중
- View : 화면을 그리는 일에 집중
JSON 데이터 형태로 데이터를 내려주는 방식 -> 클라이언트에서 JSON을 통해 html 그리기 / 서버 간의 통신 등...
@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name; // hello name -> 문자
}
// @ResponseBody 어노테이션을 통해 hello-string 엔드포인트가 요청됐을 경우,
// html의 body 부분을 통째로 넘기는 방식
@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 String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
// http://localhost:8080/hello-api?name=hihi
// 넘어온 값 -> key : value 형식
{
"name": "hihi"
}

클래스 의존관계
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;
}
}
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
// 동시성 문제로, 실무에서는 ConcurrentHashMap, AtomicLong 사용 고려
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);
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());
}
}
개발한 기능을 실행해서 테스트 할 때, main 메서드를 통해 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다.
이러한 방법은 준비와 실행에 오래 걸리고, 반복 실행 / 여러 테스트 동시 실행이 어렵다.
자바는 JUnit이라는 프레임워크로 테스트를 실행할 수 있다.
org.junit.jupiter.api.Test
assertThat(객체, 데이터).isEqualTo(비교값);
cf) TDD - 테스트 주도 개발 : 테스트 케이스를 먼저 만들어 둔 뒤, 이에 부합하는지 넣어보는 방식으로 개발을 진행 (검색해보기)
// Service의 경우, business logic이 많이 들어가므로, naming 단계에서 특정한 행동이 연상되는 동사를 많이 사용
// 반면, Repository의 경우, 데이터 처리가 단순하며, 기계적인 용어를 많이 사용한다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 회원 가입
public Long join(Member member) {
// 같은 이름의 회원 금지 로직
// result.get(); 바로 값을 꺼낼 수도 있지만, 권장하지 않는다.
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();
}
// 특정 멤버 조회 (Id)
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
class MemberServiceTest {
MemberService memberService; MemberService memberService;
MemoryMemberRepository memberRepository;oryMemberRepository memberRepository;
// 클래스 필드에 new를 통해 생성한 인스턴스를 사용하면,
// 굳이 여러 인스턴스를 통해 테스트하고, 값을 저장할 필요가 없으므로
// Service에서 MemoryMemberRepository를 new를 통해 생성하지 말고,
// 생성자를 통해 외부의 값을 사용하는 것이 좋다 (Dependency Injection - DI)
// BeforeEach를 통해 Repository를 생성하고, 생성된 인스턴스를 MemberService에 주입하여 사용
@BeforeEach@BeforeEach
public void beforeEach(){public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach@AfterEach
public void afterEach(){public void afterEach(){
memberRepository.clearStore();
}
@Test@Test
void 회원가입() {void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
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));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// try{ try - catch로 test 확인
// memberService.join(member2);
// fail();
// } catch (IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
//then
}
}
지금까지, Member와 Repository, 그리고 Service를 통해 서로 의존관계를 형성하고 회원 가입 및 조회 기능과 그 테스트를 진행.
컨트롤러를 통해 Service의 데이터를 View 단으로 보내고, html(템플릿 엔진)로 뿌리기
@Controller
public class MemberController {
}
@Controller 어노테이션을 붙이면, 스프링 컨테이너에 MemberController를 객체화하여 Spring에서 관리 (Spring Container에서 Spring Bean이 관리된다!)
이미지

MemberService의 경우, 여러 번 생성할 필요 없이 하나만 생성하여 공유해 사용하면 된다. Spring Container에 등록한 뒤 이를 @Autowired로 땡겨와서 사용
@Autowired를 사용하기 위해서는 MemberService를 Spring이 인식할 수 있도록 바꿔야 한다. @Service 어노테이션 사용!
Repository도 마찬가지로 @Repository 어노테이션 사용

이처럼 각각의 객체를 어노테이션을 통해 Spring Container에 담아 관리하고, 서로의 의존관계를 형성하는 것 = Dependency Injection
@Autowired를 통해 Spring이 DI를 관리하고, 제어하는 것 = IoC (Inversion of Control)
아래와 같이 인터페이스에 Autowired를 걸어두면, 실제로 구현된 Repository(@Repository가 있는)를 Container에서 끌어옴
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
IoC (Inversion of Control, 제어의 역전) : 소프트웨어 설계 원칙 중 하나로 프로그래밍에 있어
객체의 생성 및 관리 책임을 개발자에서 전체 애플리케이션 또는 프레임워크에 위임하는 디자인 원칙.
개발자가 모든 것을 제어하는 것에서 프레임워크가 개발자의 프로그래밍을 제어하는 형태 (인터페이스의 구현, 어노테이션 제약 등등...)
컴포넌트 스캔과 자동 의존관계 설정
- @Component : 등록된 객체를 Spring Bean으로 자동 등록
- @Service, Repository, Controller 모두 @Component 어노테이션이 포함되어 있음
- 컴포넌트 스캔 : 컴포넌트 어노테이션이 붙은 객체를 확인
- 참고) 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤 등록(단 한 개만 등록) -> 같은 스프링 빈은 같은 인스턴스
자바 코드로 직접 스프링 빈 등록하기
- SpringConfig Class 만들기
- @Configuration 어노테이션 등록
- Service와 Repository를 @Bean을 통해 Spring에 등록하기-
컴포넌트 스캔이 아닌 직접 스프링 빈을 등록했을 때, Repository의 변경 (변경사항 발생)시, return new MemoryRepository를 DbRepository로 바꾸기만 하면된다. (변경할 부분 최소화)
setter injection -> public이므로, 중간에 값 변경 가능성 존재
1. DataSouurce 생성하기 (생성자를 통해)
2. 리포지토리 내용 구현하기
- sql 문장 작성
- Connection, PreparedStatement, ResultSet 준비하기
- getConnection, disConnection 등을 사용하여 연결 및 해제
@SpringBootTest / @Transactional
객체의 경우, 생성자를 통한 DI도 가능하지만, test는 가장 끝 단에 위치한 작업으로 필드 와이어드를 통해 간단하게 사용하면 편하다!
@SpringBootTest를 통해 Spring Container 사용 + Test
@Transcational을 통해 DB에 테스트 당시의 데이터가 Commit되지 않도록 RollBack하기!
단! 순수한 단위 테스트 (자바로만 짜여진, 스프링을 사용하지 않는)가 더 빠르고, 좋은 테스트이므로, 단위테스트를 잘 만드는 연습을 하자!
순수 jdbc와 동일한 환경설정 / 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. But! SQL은 직접 작성
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
// 단! 생성자가 1개인 경우, 생략 가능// 단! 생성자가 1개인 경우, 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
// SimpleJdbcInsert -> usingGeneratedKeyColumns를 통해, column이름을 사용하면,
// query문 사용 없이 사용할 수 있다.
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
private RowMapper<Member> memberRowMapper(){
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
- Java에서 DB에 연결 요청
- 인증 후, 세션 오픈
- Connection 유지한 채, 쿼리 전송
- DB에서 데이터 조작
- Data를 Java 객체로 변경
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
Optional<Member> findByName(String name);
}
// 인터페이스로 생성된 스프링 데이터 JPA는 JpaRepository와 기존 Repository를 상속하여 설정 (인터페이스는 다중 상속 가능)
// <도메인 객체, id타입> 지정
// findByName, findAll 등, 자주 사용되는 CRUD 기능 탑재
// 복잡한 쿼리의 경우, Querydsl 라이브러리를 활용하거나, 기존의 JdbcTemplate를 통해 작성
@Configuration
public class SpringConfig {
// 스프링 데이터 JPA가 자동으로 SpringDataJpaRepository를 스프링 빈으로 등록
// MemberRepository(interface)를 주입해도, 객체 생성 가능!
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}

핵심 비즈니스 로직이 아닌, 로직에 대해 여러 부분에 공통적으로 적용되어야 하는 로직을 AOP로 만들어서 동시 적용 가능
AOP: Aspect Oriented Programming (관점 지향 프로그래밍), 공통 관심 사항과 핵심 관심 사항을 분리
ex)

