[인프런 김영한] Spring 입문

Dyong_Song·2024년 2월 20일

📕김영한님 Spring 입문 강의

프로젝트 사용기술

스프링부트 / JPA / Gradle / HIBERNATE / Thymeleaf

📌1. 프로젝트 환경 설정

스프링 환경설정: 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 만들기

스프링 웹 동작 순서

  1. URL /hello 입력
  2. 톰캣 서버로 전송
  3. 톰캣에서 스프링 컨트롤러에 매핑된 "hello"를 찾아가서 메서드 실행
  4. 메서드에서 Model이 데이터를 옮긴다. (Key / Value 형식)
  5. return "hello" -> hello.html로 전송
    • 컨트롤러에서 return값으로 문자를 반환하면 ViewResolver가 화면을 찾아서 처리
    • 스프링 부트의 ViewResolver 기본 매핑: resources:templates/ + {ViewName} + .html
  6. 템플릿 엔진은 받은 데이터를 통해 html을 새롭게 그린 뒤 서버로 전송.
  7. 웹 브라우저에 hello.html 출력 (Model에서 전송한 데이터들을 html에 출력한 채로)

빌드하기

인텔리제이 안에서 실행한 것과 달리, 명령줄을 통해 빌드하기

  • 해당 프로젝트 dir로 이동후, build
./gradlew build (Linux)
gradlew.bat build (cmd)
  • jar 파일로 서버 돌리기 (cd ../build/libs)
java -jar xxxxxxx.jar
  • 빌드 폴더 지우기
./gradlew clean

📌2. 스프링 웹 개발 기초

정적 컨텐츠

html 파일을 서버에 그대로 내려주는 형태

MVC와 템플릿 엔진

가장 많은 방식 / JSP 혹은 PHP와 같이 템플릿 엔진을 통해 서버에서 프로그래밍을 한 뒤, html을 동적으로 바꿔서 내려주는 형태 (Model - View - Controller)
cf) Model1 방식 -> View에서 모든 것을 처리 (코드 가독성 하향)

  • Controller : 데이터 처리, 비지니스 로직 관리에 집중
  • View : 화면을 그리는 일에 집중

API 방식

JSON 데이터 형태로 데이터를 내려주는 방식 -> 클라이언트에서 JSON을 통해 html 그리기 / 서버 간의 통신 등...

@GetMapping("hello-string")
@ResponseBody
public String helloString(@RequestParam("name") String name) {
    return "hello " + name; // hello name -> 문자
}
// @ResponseBody 어노테이션을 통해 hello-string 엔드포인트가 요청됐을 경우,
// html의 body 부분을 통째로 넘기는 방식
  • 사용 이유?
    - json 방식으로 값을 편하게 넘겨주기 위해
    - 객체 자체를 반환하면, JSON 형식으로 바뀌어서 넘어간다 (Default 값)
@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"
}
  • 동작 원리 : @ResponseBody가 있으면, ViewResolver에 넘겨 html을 렌더링 하는 것이 아닌, HttpMessageConverter 를 통해 JSON 형식으로 변환해서 전송
  • 렌더링 2가지 방식
    - html 형식으로 보내기 (정적 / MVC)
    - API 방식으로 보내기 (API)

📌3. 회원 관리 예제

비즈니스 요구사항 정리

일반적인 웹 애플리케이션 계층 구조

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

클래스 의존관계

  • 아직 DB가 선정되지 않았기에, 인터페이스로 구현 클래스를 변경하도록 설계
  • DB는 RDB, NoSQL 등의 저장소를 고려 중인 시나리오
  • 개발 진행을 위한 메모리 기반의 데이터 저장소 사용 예정

회원 도메인과 리포지토리 만들기

회원 객체

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이라는 프레임워크로 테스트를 실행할 수 있다.

  • Test 방법
  1. @Test 어노테이션 사용 (Junit API)
org.junit.jupiter.api.Test
  1. Assertions.assertThat()을 통해 값 비교
assertThat(객체, 데이터).isEqualTo(비교값);
  1. Unit 단위 or 전체 테스트를 진행하여, 통과 여부 확인
  2. 데이터가 겹치는 경우, @AfterEach, @BeforeEach 어노테이션을 활용해 데이터 정리

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

📌4. 스프링 빈과 의존관계

컴포넌트 스캔과 자동 의존관계 설정

지금까지, 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, 제어의 역전) : 소프트웨어 설계 원칙 중 하나로 프로그래밍에 있어
객체의 생성 및 관리 책임을 개발자에서 전체 애플리케이션 또는 프레임워크에 위임하는 디자인 원칙.
개발자가 모든 것을 제어하는 것에서 프레임워크가 개발자의 프로그래밍을 제어하는 형태 (인터페이스의 구현, 어노테이션 제약 등등...)

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

  • 컴포넌트 스캔과 자동 의존관계 설정
    - @Component : 등록된 객체를 Spring Bean으로 자동 등록
    - @Service, Repository, Controller 모두 @Component 어노테이션이 포함되어 있음
    - 컴포넌트 스캔 : 컴포넌트 어노테이션이 붙은 객체를 확인
    - 참고) 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤 등록(단 한 개만 등록) -> 같은 스프링 빈은 같은 인스턴스

  • 자바 코드로 직접 스프링 빈 등록하기
    - SpringConfig Class 만들기
    - @Configuration 어노테이션 등록
    - Service와 Repository를 @Bean을 통해 Spring에 등록하기-

  • 컴포넌트 스캔이 아닌 직접 스프링 빈을 등록했을 때, Repository의 변경 (변경사항 발생)시, return new MemoryRepository를 DbRepository로 바꾸기만 하면된다. (변경할 부분 최소화)

setter injection -> public이므로, 중간에 값 변경 가능성 존재

📌5. Front 추가

📌6. 스프링 DB 접근 기술

순수 JDBC

환경설정

  • build.gradle 에 jdbc, h2 관련 라이브러리 추가하기
    - implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    - runtimeOnly 'com.h2database:h2'
  • 스프링 부트 데이터베이스 연결 설정 추가하기
    - application.properties에 설정하기
    - spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    - spring.datasource.driver-class-name=org.h2.Driver
    - spring.datasource.username=sa
  • jdbc 리포지토리 구현하기!
1. DataSouurce 생성하기 (생성자를 통해)
2. 리포지토리 내용 구현하기
    - sql 문장 작성
    - Connection, PreparedStatement, ResultSet 준비하기
    - getConnection, disConnection 등을 사용하여 연결 및 해제
  • 스프링 설정 변경하기
    - SpringConfig에서 기존에 @Bean을 통해 연결한 Repository 대신 JdbcRepository 연결하기
  • OCP 원칙 -> 스프링 DI를 통해 기존 코드 변경 없이, 인터페이스를 통한 기능 확장 가능

스프링 통합 테스트

@SpringBootTest / @Transactional
객체의 경우, 생성자를 통한 DI도 가능하지만, test는 가장 끝 단에 위치한 작업으로 필드 와이어드를 통해 간단하게 사용하면 편하다!
@SpringBootTest를 통해 Spring Container 사용 + Test
@Transcational을 통해 DB에 테스트 당시의 데이터가 Commit되지 않도록 RollBack하기!

단! 순수한 단위 테스트 (자바로만 짜여진, 스프링을 사용하지 않는)가 더 빠르고, 좋은 테스트이므로, 단위테스트를 잘 만드는 연습을 하자!

스프링 jdbcTemplate

순수 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;
        };
    }
  • 기존의 Connection, ResultSet 등을 RowMapper를 통해 축소
  • query에 사용할 쿼리문과 rowMapper객체, ?에 들어갈 매개변수값을 넣어주기
  • SimpleJdbcInsert를 통해 query없이도 작업 가능

JPA (Java Persistence API)

JPA란?

  • JPA는 기존의 반복 코드 제거와 기본적인 SQL 직접 작성
  • SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임 전환
  • 개발 생산성 증가
  • 자바의 표준 인터페이스이며, 구현은 각 업체에서 담당 (hibernate, EclipseLink, OpenJPA)

ORM 기술

  • JPA는 ORM 기술
  • ORM (Object Relational Mapping) : 객체를 데이터베이스의 데이터와 연결(매핑)
  • DB에서 데이터를 받기 위해, Java에서 코드(모델링)를 통해 객체에 데이터를 받을 수 있다.
  • 반대로, Java에서 DB 데이터 테이블을 만들 수 있다.
  • 두 과정을 자동으로 해주는 ORM 기술

ORM 없는 DB 조작 순서도

  1. Java에서 DB에 연결 요청
  2. 인증 후, 세션 오픈
  3. Connection 유지한 채, 쿼리 전송
  4. DB에서 데이터 조작
  5. Data를 Java 객체로 변경

사용법

  1. build.gradle에 JPA와 DB 라이브러리 추가
  2. application.properties(스프링 부트 설정)에 jpa 쿼리 설정
    • spring.jpa.show-sql=true --> jpa가 생성하는 sql문 출력
    • spring.jpa.hibernate.ddl-auto=none --> 테이블을 자동 생성하는 기능 끄기
  3. domain 클래스에 @Entity (javax.persistence.Entity) 어노테이션을 통해 JPA가 관리하는 Entity로 만들기
    • @Id (PK 설정) @GeneratedValue(strategy = GenerationType.IDENTITY -> DB에서 생성되는 Id를 의미)
    • DB의 컬럼명과 다를 경우: @Column(name = "xxxx")
  4. JpaRepository 만들기
    • private final EntitiyManager em;
    • JPA는 EntityManager를 통해 모든 동작을 수행
    • build.gradle에서 받은 jpa 라이브러리를 통해 spring boot가 EntityManager 생성 후, DB와 연결
    • DB 통신, 커넥션 관리 등 properties 설정 값들을 반영하여 Manager 내부에서 모든 작업을 처리
    • 즉, JPA를 쓰기 위해서는 EntityManager를 주입받아야 한다.
  5. 쿼리문 작성법
    • em.persist() -> insert 쿼리 작성, setId등의 작업을 모두 한 뒤 DB에 저장
    • em.find(객체.class, 컬럼명) -> 컬럼명을 통해서 해당 데이터를 객체로 반환 -> Optional.ofNullable(객체) => PK 컬럼의 경우 em.find를 통해 가능 / 전체 조회나 특정 컬럼을 통한 조회는 JPQL 작성 필요!
    • JPQL (객체지향 QL) 예시 -> em.createQuery("select a from 객체 a", 객체.class).getResultList(); => 테이블 대상 sql이 아닌, 객체(Entity)를 대상으로 퀴리 요청 => 객체가 가진 필드들의 값을 채워서 리턴
    • JPQL 예시2 -> List result = em.createQuery("select a from 객체 a where a.컬럼명 = :파라미터", 객체.class).setParameter("컬럼명", 파라미터).getResultList();
      - 1개만 반환되므로, result.stream().findAny();
  6. JPA는 모든 데이터 변경이 Transaction에서 이뤄져야 한다. 따라서, Service 계층에 @Transactional 어노테이션 붙이기.
  7. SpringConfig에 Repository JPA로 변경 + EntityManager 주입받기 (@Autowired)
  8. TestCase를 통해 확인 => showSql 설정으로 인해 Manager가 생성한 쿼리 확인 가능

스프링 데이터 JPA

스프링 데이터 JPA란?

  • 스프링 부트 + JPA를 통해 개발 생산성 증가
  • 스프링 데이터 JPA를 추가하면, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발 가능
  • 반복되는 CRUD 기능도 제공

사용법

  • 스프링 데이터 JPA 리포지토리
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,  Long>, MemberRepository { 
    Optional<Member> findByName(String name);
}
// 인터페이스로 생성된 스프링 데이터 JPA는 JpaRepository와 기존 Repository를 상속하여 설정 (인터페이스는 다중 상속 가능)
// <도메인 객체, id타입> 지정
// findByName, findAll 등, 자주 사용되는 CRUD 기능 탑재
// 복잡한 쿼리의 경우, Querydsl 라이브러리를 활용하거나, 기존의 JdbcTemplate를 통해 작성
  • 스프링 데이터 JPA 리포지토리로 스프링 설정 변경
@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);
    }
}
  • 스프링 데이터 JPA 클래스 및 메서드

📌7. AOP (Aspect Oriented Programming)

AOP가 필요한 상황

  1. 모든 메소드의 호출 시간 측정하기
  2. 공통 관심 사항 (cross-cutting concern) vs 핵심 관심 사항 (core concern)
  3. 회원 가입 시간, 회원 조회 시간 측정

핵심 비즈니스 로직이 아닌, 로직에 대해 여러 부분에 공통적으로 적용되어야 하는 로직을 AOP로 만들어서 동시 적용 가능

AOP 적용

AOP: Aspect Oriented Programming (관점 지향 프로그래밍), 공통 관심 사항과 핵심 관심 사항을 분리
ex)

스프링의 AOP 동작 방식

  • AOP 적용 전 의존 관계
  • AOP 적용 후 의존 관계 ( 프록시라는 다른 스프링 빈을 생성 )
profile
꾸준한 개발자가 되자! Do steady yong

0개의 댓글