Backend/Spring 인강

스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술

kms152000 2024. 4. 13. 05:40

1. 프로젝트 환경설정

 

1-1. 프로젝트 생성

https://start.spring.io 접속해서 프로젝트 생성

Group: 기업명(도메인명)등 

Artifact: 프로젝트명 

Dependencies에 Spring web + Thymeleaf(html을 만드는 템플릿) 추가

GENERATE 후 압축 풀고 IntelliJ에서 build.gradle파일 Open

 

build.gradle의 repositories의 mavenCentral은 라이브러리들을 다운로드하는 사이트 주소 

 

Project Options - Tree Appearance - Compact Middle Packages: 해당 패키지나 파일명만 보여준다.

 

@SpringBootApplication에 톰캣 서버 내장

 

Preference - Gradle - Gradle을 IntelliJ로 변경(실행 속도 향상)

 

localhost:8080 접속해서 연결 확인

 

 

 

 

1-2. 라이브러리 살펴보기

Gradle은 의존관계가 있는 라이브러리를 함께 다운로드 한다.

 

스프링 부트 라이브러리

  • spring-boot-starter-web
    • spring-boot-starter-tomcat: 톰캣 (웹서버)
    • spring-webmvc: 스프링 웹 MVC
  • spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
  • spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
    • spring-boot
      • spring-core
    • spring-boot-starter-logging
      • logback, slf4j

테스트 라이브러리

  • spring-boot-starter-test
    • junit: 테스트 프레임워크
    • mockito: 목 라이브러리
    • assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
    • spring-test: 스프링 통합 테스트 지원

 

 

 

1-3. View 환경설정

static/index.html을 올려두면 Welcome page 기능을 제공한다.

 

  • 컨트롤러에서 리턴 값으로 문자를 반환하면 뷰 리졸버( viewResolver )가 화면을 찾아서 처리한다.
    • 스프링 부트 템플릿엔진 기본 viewName 매핑
    • resources:templates/ +{ViewName}+ .html

 

 

 

 

2. 스프링 웹 개발 기초

 

2-1. 정적 컨텐츠

스프링 부트는 정적 컨텐츠를 기본 제공한다.

https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-spring-mvc-static-content

 

Spring Boot Features

Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and Servlet-based web applications. It occurs as part of closing the application context and is performed in the earliest

docs.spring.io

정적 컨텐츠 작동원리

 

 

 

 

2-2. MVC와 템플릿 엔진

 

 

 

 

 

 

2-3. API

@ResponseBody: http의 Body부분에 직접 데이터를 넣어주는 어노테이션 

  • @ResponseBody를 사용하면 viewResolver 대신에 HttpMessageConverter가 동작
  • 대신에 HTTP의 BODY에 문자 내용을 직접 반환(HTML BODY TAG를 말하는 것이 아님)

 

ResponseBody 사용원리

 

 

3. 회원 관리 예제 - 백엔드 개발

 

3-1. 비즈니스 요구사항 정리

  • 데이터: 회원 ID, 이름
  • 기능: 회원 등록, 조회
  • 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

 

  • 컨트롤러: 웹 MVC의 컨트롤러 역할
  • 서비스: 핵심 비즈니스 로직 구현
  • 리포지토리: 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인: 비즈니스 도메인 객체, 예) 회원, 주문, 쿠폰 등등 주로 데이터베이스에 저장하고 관리됨

 

  • 아직 데이터 저장소가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
  • 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정
  • 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

 

 

3-2. 회원 도메인과 리포지토리 만들기

 

회원 객체

package hello.hellospring.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;
	}
}

 

 

 

회원 리포지토리 인터페이스

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);
	Optional<Member> findByName(String name);
	List<Member> findAll();
}

Optional <>: null값을 반환하는 방법

 

 

회원 리포지토리 메모리 구현체

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import java.util.*;
/**
 * 동시성 문제가 고려되어 있지 않음, 실무에서는 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 List<Member> findAll() {
		return new ArrayList<>(store.values());
	}

	@Override
	public Optional<Member> findByName(String name) {
		return store.values().stream()
		.filter(member -> member.getName().equals(name))
		.findAny();
	}

	public void clearStore() {
		store.clear();
	}
}

 

 

 

 

3-3. 회원 리포지토리 테스트 케이스 작성

 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 반복 실행하기 어렵고 여러 테스트를 한 번에 실행하기 어렵다는 단점을 해결한다.

 

 

src/test/java 하위 폴더에 생성한다.

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
	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);
	}
}
  • @AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach를 사용하면 각 테스트가 종료될 때마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다.
  • 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아니다.

 

 

 

3-4. 회원 서비스 개발

 

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;

public class MemberService {

	private final MemberRepository memberRepository = new
	MemoryMemberRepository();
	/**
	* 회원가입
	*/
	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);
	}
}

ifPresent: 값이 있으면 로직이 동작

 

 

 

 

 

3-5. 회원 서비스 테스트

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
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("이미 존재하는 회원입니다.");
	}
}

@BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하 고, 의존관계도 새로 맺어준다

 

 

 

 

 

4. 스프링 빈과 의존관계

 

4-1. 컴포넌트 스캔과 자동 의존관계 설정

package hello.hellospring.controller;

import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
	
    private final MemberService memberService;
	
    @Autowired
	public MemberController(MemberService memberService) {
		this.memberService = memberService;
	}	
}

 private final MemberService memberService; 이전에  private final MemberService memberService = new MemberService;로 쓰지 않는 이유: Membercontroller가 아닌 다른 여러 컨트롤러들이 멤버서비스를 쓸 수 있다.

 

스프링 빈을 등록하는 2가지 방법

  • 컴포넌트 스캔과 자동 의존관계 설정
  • 자바 코드로 직접 스프링 빈 등록하기

 

컴포넌트 스캔 원리

  • @Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
  • @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다.
  • @Component를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다
    • @Controller
    • @Service
    • @Repository

 

컨트롤러와 서비스를 연결시켜 주기 위해서 @Autowired를 사용한다.

memberController가 생성이 될 때 스프링 빈에 등록 돼있는 memberService 객체를 주입해 준다 -> dependency injection

 

 

 

 

4-2. 자바 코드로 직접 스프링 빈 등록하기

회원 서비스와 회원 리포지토리의 @Service, @Repository, @Autowired 어노테이션을 제거하고 진행 한다.

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());
	}
	
    @Bean
	public MemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
}

 

@Bean: 직접 스프링 빈에 등록한다.

 

  • DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. 의존관계가 실행중에 동적으 로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.
  • 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그 리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.

 

 

 

 

5. 회원 관리 예제 - 웹 MVC 개발

 

5-1. 회원 웹 기능 - 홈 화면 추가

홈 컨트롤러 추가

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>

컨트롤러가 정적 파일보다 우선순위가 높다.

 

5-2. 회원 웹 기능 - 등록

회원 등록 폼 개발

회원 등록 폼 개발

@Controller
public class MemberController {

	private final MemberService memberService;
	
    @Autowired
	public MemberController(MemberService memberService) {
		this.memberService = memberService;
	}

	@GetMapping(value = "/members/new")
	public String createForm() {
		return "members/createMemberForm";
	}
}

get: 조회할 때 주로 쓴다.

 

회원 등록 폼 HTML ( resources/templates/members/createMemberForm )

<!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>

name="name"은 아래 class MemberForm의 getName(), setName() 메소드로 값을 등록하고 받아온다.

 

회원 등록 컨트롤러

웹 등록 화면에서 데이터를 전달 받을 폼 객체

package hello.hellospring.controller;

public class MemberForm {
	private String name;
	
    public String getName() {
		return name;
	}
	
    public void setName(String name) {
		this.name = name;
	}
}

 

회원 컨트롤러에서 회원을 실제 등록하는 기능

@PostMapping(value = "/members/new")
public String create(MemberForm form) {

	Member member = new Member();
	member.setName(form.getName());
	
    memberService.join(member);
	
    return "redirect:/";
}

post: 등록할 때 주로 쓴다.

redirect:/: 홈 화면으로 리턴한다.

 

MemberForm은 위 클래스를 말한다.

 

 

 

5-3. 회원 웹 기능 - 조회

회원 컨트롤러에서 조회 기능

@GetMapping(value = "/members")
public String list(Model model) {
	List<Member> members = memberService.findMembers();
	model.addAttribute("members", members);
	return "members/memberList";
}

회원 리스트 HTML

<!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>

 

 

 

 

 

 

 

6. 스프링 DB 접근 기술

 

6.1 - H2 데이터베이스 설치

개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공

https://www.h2database.com

 

H2 Database Engine (redirect)

H2 Database Engine Welcome to H2, the free SQL database. The main feature of H2 are: It is free to use for everybody, source code is included Written in Java, but also available as native executable JDBC and (partial) ODBC API Embedded and client/server mo

www.h2database.com

실행: 윈도우 사용자는 h2.bat

데이터베이스 파일 생성 방법

  • jdbc:h2:~/test (최초 한번)
  • ~/test.mv.db 파일 생성 확인
  • 이후부터는 jdbc:h2:tcp://localhost/~/test 이렇게 접속

 

테이블 생성하기

테이블 관리를 위해 프로젝트 루트에 sql/ddl.sql 파일을 생성

drop table if exists member CASCADE;
create table member
(
	id bigint generated by default as identity,
	name varchar(255),
	primary key (id)
);

 

 

H2 데이터베이스가 정상 생성되지 않을 때

  1. H2 데이터베이스를 종료하고, 다시 시작한다.
  2. 앞 부분만 100.1.2.3 -> localhost 로 변경하고 Enter를 입력한다. 나머지 부분은 절대 변경하면 안된다. (특히 뒤에 세션 부분이 변경되면 안된다.)

 

 

 

 

6-2. 순수 JDBC

김영한님이 정신건강을 위해서 고대에는 이렇게 코드를 작성했구나 정도를 알면 된다 하셔서 기록은 넘어간다.

 

환경설정

build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가

implementation 'org.springframework.boot:spring-boot-starter-jdbc'

runtimeOnly 'com.h2database:h2' 

 

스프링 부트 데이터베이스 연결 설정 추가 resources/application.properties  spring.datasource.url=jdbc:h2:tcp://localhost/~/test

spring.datasource.driver-class-name=org.h2.Driver

spring.datasource.username=sa

 

 

6-3. 스프링 통합 테스트

이번 강의도 편하게 들어고 된다 하셔서 기록은 넘어간다.

  • @SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
  • @Transactional: 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.

 

 

 

 

6-4. 스프링 JdbcTemplate

실무에서도 자주 사용

스프링 JdbcTemplate 회원 리포지토리

package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
    private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public Member save(Member member) {
        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> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
        return result.stream().findAny();
    }
    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from member", memberRowMapper());
    }
    @Override
    public Optional<Member> findByName(String name) {
        List<Member> result = jdbcTemplate.query("select * from member wherename = ?", 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;
        };
    }
}

 

생성자가 1개면 @Autowired 생략 가능하다.

 

 

 

 

6-5. JPA

객체와 ORM 기술

@Entity: jpa가 관리하는 엔티티

identity 전략: 프라이머리 키 자동 생성

jpa는 EntityManager로 동작

return 값이 같으면 Ctrl + Alt + N

트랜잭션 필수(조인될 때 트랜잭션 안에서 해결해야함) 

 

 

6-6. 스프링 데이터 JPA

 

 

 

 

 

 

7. AOP

 

7-1. AOP가 필요한 상황

 

 

 

7-2. AOP 적용