ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 입문 4 ~ 6주차
    BackEnd/Spring 2022. 8. 5. 12:58

    📌 스프링 빈과 의존관계

    📌 Component scan 

    - @Component 어노테이션이 붙은 코드를 스캔하여 스프링 컨테이너 빈으로 등록하는 것

    - 강의에서 사용한 @Controller, @Service, @Repository등이 @Component의 특수화된 케이스들. 

    - Application 파일 하위부터 컴포넌트 스캔이 들어가므로 default 설정은 패키지 포함 하위만 스캔하며, @ComponentScan 어노테이션을 표기해두면 상위에서도 컴포넌트 스캔 하도록 지정이 가능 

    - 스프링 컨테이너가 @Controller 보고 객체로 생성하여 관리해준다. 스프링 실행 시점에 이 @Controller 객체를 생성하여 스프링 컨테이너가 관리 

    - 위에 적혀있듯 Controller는 요청을 받는 측, Service가 비즈니스 로직들 처리, Repository가 내부 로직 처리

     

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

     

    📌 @Autowired

    실행 시점에 스프링 컨테이너가 생성자에 이 어노테이션이 붙어있다면 이 매개변수에 있는 memberService 연결을 자동으로!  해준다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection, 의존성 주입이라고 함) 

     

    📌 DI 

     

    - 참조중인 memberService는 아직 순수 자바 코드이기 때문에 이 파일도 스프링 컨테이너가 관리하도록 해줘야 함. 

     

    🤔 스프링 빈 주입 방법? 

    - @Autowired : 생성자 빼고 필드에 바로 주입 (중간에 내가 변경하기 어렵다는 단점이 존재)

    - setter (setter가 public하게 노출되며 처음 세팅외에는 호출될 일이 없는데 잘못 호출될 경우 문제 발생)

    - 💚생성자 사용(@RequiredAgrsConstructor : final이 선언된 모든 필드를 인자값으로 하는 생성자 자동생성 라이브러리)

             -> 실행 시 한 번 호출되고 끝나서 의존관계 주입이 동적으로 변하는 경우는 거의 없기 때문. (이런 경우는 config파일               자체 수정, 서버 다시구동)

     


    @Service
    public class MemberService {
     private final MemberRepository memberRepository;
     @Autowired
     public MemberService(MemberRepository memberRepository) {
     this.memberRepository = memberRepository;
     }
    }

    📌 스프링 빈 등록할 때는 기본적으로 싱글톤으로 등록(하나만 등록, 모두 같은 인스턴스)한다. 위 그림에서 예를들어, orderService처럼 다른 서비스가 memberRepository참조한다. 이러면 전에 만들어진 memberRepository(이미 스프링 컨테이너에서 생성된)애를 전달해줌. 

     


    📌 이 외에 방법으로 자바 코드로 직접 스프링 빈으로 등록하는 방법이 존재 (@Configuration) 이 방법이 더 권장

    - Spring Config등 파일을 생성하고 @Configuration어노테이션 붙이기
    - 그리고 @Bean어노테이션 등록

     

    @Configuration이 스프링 컨테이너에게 설정정보를 전달하는 어노테이션, 그리고 각 @Bean이 빈으로 등록할 객체들에 붙는 어노테이션

     

    💚 추가로, @Configuration 없이 @Bean만 사용한다면 빈 등록은 가능한데 싱글톤은 깨진다. ( EX 위의 memberRepository() 처럼 의존관계 주입이 필요해서 메소드를 직접 호출할 때 ) 그러니 항상 별다른 고민 없이 @Configuration 어노테이션을 통해 스프링 설정정보를 관리하자. 

     

    📌 왜 자바 코드로 짜는게 더 권장되는가? 

    EX) 상황에 따라 구현 클래스를 변경해야 한다(기획안이 픽스되지 않은 경우에)
    -> config클래스만 뭐 주입할지 살짝 바꿔두면 됨(인터페이스 파일을 구현하는 클래스들이라 호환 가능)
    -> component 스캔보다 config하나 바꾸는게 간단해서 ! 

     

    📌 주의사항
    스프링 빈으로 등록해놔야 autowired가 작동된다. new로 직접 객체를 생성하는 경우에도 autowired X!
    autowired는 컨테이너가 관리해야만 동작하는 것을 기억. 

     


    📌 타임리프 이용한 화면 기능들 구현

    📕 controller로 요청이 들어가 전달된다면 도메인에서 리소스를 찾고  index.html 과 같은 정적 파일들은 무시됨.

    @GetMapping(value = "/members/new")
     public String createForm() {
     return "members/createMemberForm";
     }
     
    @PostMapping(value = "/members/new")
    public String create(MemberForm form) {
     Member member = new Member();
     member.setName(form.getName());
     memberService.join(member);
     return "redirect:/";
    }

    📕 참고로 저 value에서 members가 공통된 경로니까 Controller 맨 위에 빼는 것도 가능 

    @RequestMapping(value = "/user", consumes = MediaType.APPLICATION_JSON_VALUE)
    public class UserApiController {
        private final UserService userService;
        @PostMapping("join")
        public ResponseEntity join(@RequestBody UserJoinRequestDto requestDto) {
            try {
                UserJoinResponseDto userJoinResponseDto = userService.join(requestDto);
                return ResponseEntity.ok().body(ResponseDto.res(Status.OK, Message.JOIN_SUCCESS, userJoinResponseDto));
            } catch (Exception e) {
                return ResponseEntity.badRequest().body(ResponseDto.res(Status.BAD_REQUEST, e.getMessage()));
            }
        }

    📕 아래 코드에 있는  ${members} : model.addAttribute 
    -> 이게 루프를 돌면서 (th가 타임리프 반복문) 리스트에 있는 모든 객체들을 멤버에 담고 (for each처럼) id, name 출력해줌. 

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

    📌 스프링 DB 접근 기술

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

    DB ~ long : bigint, string : varchar로 변경! (데이터 타입이 약간 다름)

    📌 참고로 mysql 계열의 db에서는 auto_increment라는 옵션을 통해 id값이 자동 생성, 증가하게끔 만든다. 

    📌 DDL : 데이터베이스 정의어 (create, drop 등)

     


    📒 순수 JDBC

    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
    runtimeOnly 'com.h2database:h2'

    -> build.gradle

    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa
    
    /////////////////////
    spring.datasource.url = 여기다 rds 주소같은거 올림.
    spring.datasource.driver-class-name = mysql, mariadb등 디비 종류 따라 다름

    -> application.properties

    📌 참고로 요즘은 application.yml 방식으로 (계층 구조로 작성 가능) 사용한다. 이렇게 설정정보 담는 파일(db url, key등등... )은 깃에 안올라가도록 잘 관리해야함.

    📌 EC2, RDS, S3등 AWS설정정보 등도 여기다 기입해둔다. 

     

    JDBC 사용방식 

    @Configuration
    public class SpringConfig {
     private final DataSource dataSource;
     public SpringConfig(DataSource dataSource) {
     this.dataSource = dataSource;
     }
     ...

    - DataSource사용하도록 Spring Config 파일 변경

    - DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둬서 DI를 받을 수 있다

    public Member save(Member member) {
     String sql = "insert into member(name) values(?)";
     Connection conn = null;
     PreparedStatement pstmt = null;
     ResultSet rs = null;
     try {
     conn = getConnection();
     pstmt = conn.prepareStatement(sql,
    Statement.RETURN_GENERATED_KEYS);
     pstmt.setString(1, member.getName());
     pstmt.executeUpdate();
     rs = pstmt.getGeneratedKeys();
     if (rs.next()) {
     member.setId(rs.getLong(1));
     } else {
     throw new SQLException("id 조회 실패");
     }
     return member;
     } catch (Exception e) {
     throw new IllegalStateException(e);
     } finally {
     close(conn, pstmt, rs);
     }
     }

    - 코드 일부만 발췌해옴. Connection 등의 자원들은 사용후에는 꼭 반납. (close) 실제로 연결하는 것이기 때문이다. 
    - tmi)  connectionpool 만들어서 사용도 가능, 미리 일정량의 connection 을 생성해두고(pool) 그때그때 가져다 쓰고 pool에 다시 반납하는 형식 

     

    📌 객체 지향적인 설계는 코드 수정을 최소화해서 좋다. 
    - 다형성을 활용한다.   

     

    - SOLID 원칙

    📌 SRP : 단일 책임 원칙 한 클래스는 하나의 책임만 가진다 하나의 책임은 약간 모호(상황따라 다름), 변경이 있을 때 파급 효과가 적은가를 본다.

    📌 OCP : 개방-폐쇄 원칙 (가장 중요!) SW 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 한다. 구현을 하나 더 하는 것은 확장하는 것, 하지만 기존 코드의 변경은 X 객체를 생성하고, 연관관계를 맺어주는 것을 Service에서 하지 않고 스프링 컨테이너가!

    📌 LSP : 리스코프 치환 원칙 상위 타입의 인스턴스는 하위 타입의 인스턴스로 바꿀 수 있어야 한다. (컴파일 성공을 넘어서서!)

    📌 ISP : 인터페이스 분리 원칙 특정 클라이언트를 위한 인터페이스 여러 개가 하나의 범용 인터페이스보다 낫다.

    📌 DIP : 의존관계 역전 원칙, 프로그래머는 추상화에 의존, 구체화에 의존하면 안된다. 구현 클래스에 의존하지 말고, 인터페이스에 의존하자(언제든 갈아 끼울 수 있게)

     


    @SpringBootTest
    @Transactional
    class MemberServiceIntegrationTest {
     @Autowired MemberService memberService;
     @Autowired MemberRepository memberRepository;
     @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("이미 존재하는 회원입니다.");
     }
    }

    📒테스트라서 @Autowired로 간단하게 넣어버림! 

    📒 @SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다. (통합 테스트 용 어노테이션)

    📒 @Transactional
    - insert하기 전까지는 commit이 반영이 아님. 
    - test 끝나고 rollback해버리기! -> 굳이 db에 테스트 넣은거 일일히 delete하는 것보다 낫다. 
    - test에 날리면 rollback해준다. 
    테스트 시작 전 트랜잭션을 걸고, 끝나면 커밋이 아닌 rollback을 해줌. 

     

    📒 트랜잭션? (추가)
    DB상태 변화시키는 논리적 기능 수행하기 위한 작업의 단위를 의미, SQL 문으로 접근 하는 것이 db변화하는것
    작업 : 일련의 연산, 사람이 정함, DB안정성 확보가능
    - Commit연산 : 성공적으로 끝나서 db가 일관성있는 상태일때
    - Rollback : 비정상종료, 원자성 깨져서 재시작, undo상태로 돌리는 연산

     

    📒 테스트 

    단위테스트 : 순수 자바 코드로 진행하는 테스트, 통합테스트 : 스프링 띄워서 db연결하고 진행
    단위테스트가 좋은 테스트일 확률이 높다. 컨테이너까지 올려야하는 상황이면 테스트 설계가 잘못되었을 확률이 높다. 살다보면 통합테스트도 필요하지만, 단위테스트를 잘 만드는 것이 좋다. 

    📒 단위테스트 (Unit Test)는 기능 단위의 테스트 코드를 작성하는 것을 의미

     

     

    📌 Spring JDBC template

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

    📌 simpleJdbcInsert : 쿼리 짤 필요 없이 테이블 명이랑 pk가지고 insert문 만들어주고, 쿼리의 결과 rowMapper통해 매핑.. (객체로 변경해줌) 

    🤔 하지만 여전히 쿼리는 개발자가 짜야하잖아? 


    JPA등장 : sql과 데이터 중심 설계에서 객체 중심의 설계로 패러다임 전환 가능

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    spring.datasource.driver-class-name=org.h2.Driver
    spring.datasource.username=sa
    spring.jpa.show-sql=true
    spring.jpa.hibernate.ddl-auto=none

    - jpa가 테이블을 자동으로 만들어주는데 우리는 이미 테이블 만들어서 none으로 꺼둔다. 
    - JPA가 인터페이스, 구현체가 hibernate라고 생각. 자바 진영의 표준 인터페이스, 구현은 여러 업체들이 한다고 생각하자

     

    📌 ORM : object relation mapping 객체 - 관계형 데이터베이스 매핑

    보통 CRUD는 기본적으로 구현되어 있음, pk기반아닌 다른 것들은 jpqa라는 쿼리 작성해줌.

    DB저장하거나 안하거나 할 때 (db접근이 들어갈 때) transaction어노테이션 (MemberService 파일에) 부여. 

    import org.springframework.transaction.annotation.Transactional
    @Transactional
    public class MemberService {}


    📌 entity 매핑

    package hello.hellospring.domain;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.GenerationType;
    import javax.persistence.Id;
    @Entity
    public class Member {
     @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
     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 class JpaMemberRepository implements MemberRepository {
     private final EntityManager em;
     public JpaMemberRepository(EntityManager em) {
     this.em = em;
     }
     public Member save(Member member) {
     em.persist(member);
     return member;
     }
    }

    - 굉장히 코드가 간결해짐! 



    📌 스프링 데이터 JPA는 JPA를 편리하게 사용하기 위한 도구

    package hello.hellospring.repository;
    import hello.hellospring.domain.Member;
    import org.springframework.data.jpa.repository.JpaRepository;
    import java.util.Optional;
    public interface SpringDataJpaMemberRepository extends JpaRepository<Member,
    Long>, MemberRepository {
     Optional<Member> findByName(String name);
    }


    📌 interface생성, JpaRepository<entity class, pk타입> 상속받기 : 알아서 구현체 만들어서 스프링 컨테이너에 등록해줌. 그냥 가져다 쓴다. 

    📌 인터페이스 구현시에 (JPA상속받는) 규칙
    - findByName(String name) 
    이러면 jpql이 select m from Member where m.name = ? 이런식으로 자동으로 만들어주고 sql로 번역됨

    - findBy XXX and XX 이런 식의 규칙으로 생성 가능. 


    인터페이스 이름만으로도 개발이 끝난다는 혁신적인 방법...! 대부분의 단순한 케이스들은 interface만으로도 끝남. 
    QueryDsl 사용하면 동적 쿼리 사용가능. @Query로 네이티브 쿼리 (SQL), jdbc 템플릿, mybatis등을 섞어서 사용가능!





    'BackEnd > Spring' 카테고리의 다른 글

    Spring 입문 7~8주차  (0) 2022.08.12
    Spring Component Scan  (0) 2022.08.07
    Spring Container  (0) 2022.07.31
    Spring 입문 강의 섹션 0 ~ 섹션 3  (0) 2022.07.29
    Spring Study 3주차 8장  (0) 2022.05.01
Designed by Tistory.