Spring Boot 3 & Spring Framework 6 - Section 13 : REST API를 DB에 연결, 게시물 기능 추가

이정수·2024년 8월 26일
0

Udemy학습-Spring & React

목록 보기
10/21

학습할 내용

  • REST API 핵심 기능
    • URI의 Resource에 대해 알아보기
      (ex : /users, /users/{id}/posts.. etc )

    • 상호작용하는 Http Request Method에 대해 알아보기
      (ex : GET, POST, PUT, DELETE.. etc)

    • JSON format의 RequestResponse의 구조 정의 학습하기
      Http BodyHttp Header 구조 학습

    • Request 시 알맞은 Http Status Code을 설정하여 Client에 Status Code 반환하기
      (ex : 200, 404, 500 ... etc)

    • Security , Validation, Exception Handling 학습하기.


  • REST API 고급 기능
    • Internationalization :
      Accept-Language Header를 통한 Message의 국제화( i18n, internationalization ) 기능을 제공하면서 다국어 처리하는 방법을 정의.

    • HATEOAS ( Hypermedia as the Engine of Application State )
      。API의 Response에서 Resource에 관한 link를 포함하여 Client가 동적으로 API를 탐색할 수 있도록 하는 개념.
      ▶ REST API를 이용하는 Client가 Server와 동적 상호작용이 가능하도록 하는것.

    • Versioning :
      。API가 breaking change하더라도 Client의 사용성을 유지하도록 기존 Version을 사용하거나 새로운 유형의 API가 추가될 경우 Client가 원할 때 새 버전의 API로 변경할 수 있는 유연성을 확보.

    • Documentation :
      REST API를 문서화하여 개발자가 쉽게 이해하고 테스트할 수 있도록함. ( swagger , OAS )

    • Content Negotiation :
      。Client와 Server 간 HTTP Transaction에서 Client가 header를 설정하여 원하는 Language 또는 Data format( JSON, XML , HTML 등 )을 협상하여 Server가 적절한 Response를 제공하는 과정.

    • Static Filtering
      Filtering : Spring Bean의 Field 중 Http Response Body에서 선택된 Field만 반환하도록 Customizing

      。동일한 Spring Bean에 대해 각각의 다른 REST API 에 대해서도 동일한 filtering을 적용.

    • Dynamic Filtering
      。특정 REST API의 Spring Bean에 대해서 Filtering을 Customizing.

    • Monitoring
      Spring Boot Actuator을 활용하여 API를 Monitoring.


  • REST API를 DB에 연결하는 방법
    • JPAHibernate를 활용하여 H2 , MySQL DB 연결하기


  • 번외
    • Spring SecurityAuthentication 구현

JPA와 Hibernate를 활용하여 DB에 연결하기.
H2 DB(In-Memory DB)로 선행적으로 CRUD를 수행 후 DB로 전환하기.
DB관련 학습내용 , H2-DB 초기설정
。dependency의 경우 Spring Project를 생성 시 이미 정의했으나 필요한 경우 h2, jpa를 정의하기.
JPAHibernate를 생성할 경우 필요.

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
  <!--jar파일의 일부에 h2 db가 포함되지 않도록 scope를 설정.-->
            <scope>runtime</scope>
</dependency>

。이후 h2-console을 사용 설정 및 동적 JDBC URL을 정적 JDBC URL로 변경

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:testdb
  • JPA를 통해 Bean을 DB Table에 Mapping
    @Entity(name="DBTable명") : JPA 관련 학습내용
    Spring JPA에 의해 해당 Annotation이 선언된 Spring Bean을 특정 DB Table과 Mapping
    。Spring Boot Auto Configuration에 의해 @Entity가 발견 될 경우, 자동으로 H2 DB에 Table을 생성.
    。JPA의 @Entity는 반드시 기본 생성자(no-arg constructor)를 지녀야한다.
@Entity(name="user_details")
public class User {
    @Id
    // Pk값을 자동 할당.
    @GeneratedValue 
    private int id;
    // JPA 정의를 위해 생성한 기본 생성자
    public User(){}
    @Size(min=2, message = "Name should have at least 2 characters.")
    @JsonProperty("user_name")
    private String name;
    @Past(message = "Birth Date should be in the past.")
    @JsonProperty("birth_date")
    private LocalDate  birthDate;
    public User(int id, String name, LocalDate birthDate) {
        this.id = id;
        this.name = name;
        this.birthDate = birthDate;
    }

  • user_details 이름의 table이 H2 DB상에서 생성됨.
    • @JsonProperty("JSON속성이름") :
      Jackson 라이브러리에서 제공하는 Annotation으로서 JSON과 Java 객체( field ) 간 Mapping을 Control.
      Entity Class의 field에 선언 시 JSON Format으로 직렬화( Serialization )하거나, JSON Format을 field로 역직렬화( Deserialization )를 수행할 때 @JsonProperty("JSON속성이름")을 통해 field의 JSON 속성명( key )을 임의로 설정.

      。Spring Bean에 정의된 @JsonProperty에 의해 Field명이 변경될 경우 변경된 Field명으로 Http Request Body로 전송.

    • @JsonIgnore
      Jackson 라이브러리에서 제공하는 Annotation으로서 JSON직렬화/ 역직렬화 시 특정 field를 제외하는 Annotation.
      ▶ Response할 JSON data에서 선언한 데이터를 HTTP Response에서 제외.


  • src/main/resources에 초기화 데이터가 작성된 data.sql을 작성.
    data.sql :
    DB Table에 삽입할 초기 데이터를 정의하는 파일
    schema.sql :
    DB Schema를 정의하는 파일.
insert into user_details(id,name,birth_date)
values(1,'wjdtn1',CURRENT_DATE()),
         (2,'wjdtn2',CURRENT_DATE()),
         (3,'wjdtn3',CURRENT_DATE()),
         (4,'wjdtn4',CURRENT_DATE());

User Class의 birthDate field는 DB상에서 대,소문자 구분을 위해 임의로 birth_date로 명명되어 Table에 정의됨.
。이때 DataSource의 초기화를 지연하면서 JPA의 Auto-Configuration을 통해 Table이 생성된 이후( hibernate 초기화 이후 ) data.sql이 실행되도록 변경하기위해 다음 구문을 선언

spring.jpa.defer-datasource-initialization=true


h2-console에서 @Entity을 선언함으로써 JPA의 Auto-Configuration에 의해 DB상에서 자동 생성된 Table에 data.sql에서 정의한 데이터가 반영됨을 확인 가능.

  • JpaRepository를 상속하는UserRepository 생성. Spring JPA
    JpaRepository<DBEntity class, id data Type>를 상속하는 Interface를 생성.
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User,Integer> {}

Integer vs int

  • 기존의 Static Todo List가 아닌 Spring JPA에 의해 DB Table과 Mapping된 Controller Class 구축하기.
    Controller Class의 기존의 Static List가 정의된 Class( = UserDaoService) 객체를 UserRepository 객체로 대체한 후 코드 수정.
    findById() method 사용 시 JpaRepository<> interface의 객체로 변경함에 따라 해당 data type을 Optional<User>로 설정해야한다.
    ▶ 또는 findById().get()으로 User Class의 instance로 가져올 수 있다.

    JpaRepositoryfindById() 사용 시 Optional<T>로 return하는 이유?
    ▶ 조회한 DB Entity가 존재하지 않을 수 있으므로 ( null 방지용 )
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.List;
import java.util.Optional;
@RestController
public class UserResource {
    UserRepository userRepository;
    // @Autowired  Spring Context가 생성자 기반 Denpendency Injection을 수행.
    public UserResource(UserRepository userRepository) {
    this.userRepository = userRepository;
    }
    // GET Method 구현
    // GET /users : 모든 사용자 조회
    @GetMapping(path="/jpa/users")
    public List<User> ListAllUsers(){
        List<User> users = userRepository.findAll();
        return users;
    }
    // GET /users/{id} : 특정 사용자 조회
    @GetMapping(path="/jpa/users/{id}")
    public EntityModel<User> GetUserById(@PathVariable int id){
        // JpaRepository객체.findById() method 사용 시
        // return 되는 data type을 Optional<Bean>로 설정.
        Optional<User> user = userRepository.findById(id);
        // User user = userRepoistory.findById(id).get();
        if(user.isEmpty()){ // 해당 id가 존재하지 않는 경우 발생.
            throw new UserNotFoundException("id :" + id);
        }
        // HATEOAS
        EntityModel<User> entityModel = EntityModel.of(user.get());
        WebMvcLinkBuilder link = linkTo(methodOn(this.getClass()).ListAllUsers());
        entityModel.add(link.withRel("all-users"));
        return entityModel;
    }
    // POST Method 구현
    // POST /users : 새로운 사용자 생성
    @PostMapping(path="/jpa/users")
    public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
        // @RequestBody : HttpRequest의 Body(=JSON Format)를 Bean으로 변환하여 저장.
        User savedUser = userRepository.save(user);
        //REST API에서 알맞은 Response Status를 Client에게 반환하는 코드
        URI locationHeader = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{id}").buildAndExpand(savedUser.getId()).toUri();
        // 201 : created status에 해당하는 ResponseEntity<User> 객체를 생성하여 반환.
        return ResponseEntity.created(locationHeader).build();
    }
    @DeleteMapping(path="/jpa/users/{id}")
    public void DeleteUserById(@PathVariable int id){
        userRepository.deleteById(id);
    }
}
  • REST API의 Test는 API Tester를 이용.
    GET Request의 경우 브라우저를 통해 Mapping된 URL에 접속이 가능하지만 GET 이외의 경우 API Tester를 이용.
    • JpaRepository<> interface가 제공하는 Method : Spring JPA

    • Optional<T> :
      null값을 직접 다루는 대신 값이 존재할 수 있고 없을 수 있는 상황을 명확히 표현하는데 사용하는 Class
      Repository객체.findById(id)를 통해 Optional<해당Bean> 생성이 가능.
      Optional객체.get()을 통해 해당 Spring Bean을 획득 가능.
      • optional객체.isEmpty() :
        。해당 optional객체null인지 여부를 알려주는 역할을 수행하는 method.
      Optional<User> user = userRepository.findById(id);
              if (user.isEmpty()){
                  throw new UserNotFoundException("id :" + id);
              }else{
                  return user.get().getPosts();
      }
      ▶ 보통 해당 method를 활용하기위해 `Repository객체.findById(id).get()`으로 Spring Bean을 직접 가져오기보다는 `Repository객체.findById(id)`를 통해 `Optional<T>` type으로 가져오는걸 선호.


  • POST Method로 Http Request Body를 전송하여 Spring Bean instance를 JpaRepository<>를 통해 Spring Bean과 Mapping된 DB Table에 추가
    Content Negotiation을 통해 Client이 요구하는 XML type으로 Data를 전달 시 XML dataformat을 Application에서 정의하기 위해 jackson-dataformat-xml dependency를 추가
    。Client가 필요한 데이터를 Request하기 위해 Http Request Body@RequestBody에 의해 Controller Method의 매개변수로 Spring Bean instance로서 JSON Format으로 전송해야하므로, Content-Type Headerapplication/json로 설정하여 Request URL로 전송.
    。Spring Bean에 정의된 @JsonProperty에 의해 Field명이 변경될 경우 변경된 Field명으로 Http Request Body로 전송.
{
  "user_name" : "fa50",
  "birth_date" : "1998-03-04"
}


"id" field 는 Spring Bean Class field에 선언된 @GeneratedValue를 통해 첫번째 URL로 Request를 보낼 경우 id=1로서 자동 생성되며, 만약 id=1로서 기존 Spring Bean과 겹치는 경우 Http status code:500(웹 애플리케이션 코드의 버그:Spring JPA의 SQL오류)이 반환되며 한번 더 API 전송 시 다음 숫자인 id=2로 인가되어 전달.
▶ URL을 통해 Request를 전달 시 이미 Table 상에 존재하는 id=[1,4]에 대해서는 500을 반환하고 그 이후는 201을 반환.

H2-console로 조회 시 성공적으로 값이 들어가있음을 확인 가능.

Header AcceptContent-Type의 차이점 Content-Negotiation
。둘 다 데이터타입(MIME)을 다루는 헤더.

  • Accept :
    。Client가 서버에게 Accept Header로 특정 data type을 설정하여 전송 시 Client가 보낸 Header의 특정 data type으로만 response 해야하는 규약 설정.
  • Content-Type :
    。현재 전송하는 data가 어떤 type인지만 설명.

REST API 게시물

  • 전 단계의 REST API 기능 :
    。DB에서 모든 / 단일 User 조회
    User 생성 / 삭제

  • 현 단계의 목표 :
    REST API를 구축하여 특정 사용자의 모든 게시물을
    검색 ( GET /users/{id}/posts ) ,
    생성 ( POST /users/{id}/posts ),
    삭제 ( DELETE /users/{id}/posts )

연관관계 Mapping :
JPA에서 DB Entity 간의 연관관계를 설정.
。연관관계의 자식은 FK( 외래키 )가 있는쪽으로 설정해야한다.

  • 연관관계 방향 :
    객체지향 모델링에서는 구현하고자 하는 서비스에 따라서 단방향 / 양방향 관계인지 설정해야한다.
    단방향 관계 : 한 쪽의 DB Entity만 참조
    ex: Member에서 Team의 정보를 가져올 수 있지만, 반대는 불가능.

    양방향 관계 : 양 쪽의 DB Entity가 서로 참조
    ex: Member와 Team 각 table에서 상대 table의 정보를 가져올 수 있음.
    。데이터베이스(RDBMS)는 관계만 설정하면 자동으로 양방향 관계가 되므로 어느 Table이든 Join을 수행하면 원하는 Data를 가져오지만, JPA는 양방향 관계를 맺어야 가져올 수 있음!

  • JPADB Entity간 연관관계 Mapping의 Annotation 종류 :
    @Entity를 선언하여 DB Entity를 정의한 Spring Bean에서 사용.
    DB Entity간 연관관계를 Mapping시 부모 Entity를 기준으로 Mapping을 수행.
    。연관관계의 자식 Entity는 FK가 있는쪽으로 설정해야한다.

    • @JoinColumn(name = "Mapping 할 Entity의 외래키 Field명") :
      JPA에서 Entity간 연관관계 Mapping 시 자식 Entity의 FK를 지정하는 Annotation.
      1:N 양방향 관계 설정 시 부모 Entity에서 선언.

      @JoinColumn 예시

      @Entity
      public class Order {
          @Id
          @GeneratedValue
          private Long id;
          @ManyToOne
          @JoinColumn(name = "user_id")  // 외래 키 이름 지정
          private User user;
          // Getter, Setter 생략
      }

      。다음의 경우 Order Entity의 user field와 User Entity의 user_id field가 N:1 연관관계를 가진다.
      User Entity의 user_idFK로 활용됨.



    • @OneToMany
      DB Entity간 양방향 관계 설정 시 부모 Entity에서 1:N 관계를 Mapping할 때 자식 Entiy에 의해 pk로서 참조되는 fk field에서 선언하는 JPA Annotation

      @OneToMany(mappedBy="자식 Entity의 Mapping된 FK Field명") :
      。연관관계의 주인을 지정하여 외래키를 어느 Entity에서 관리할지 결정.
      ▶ 기본적으로 자식테이블에 외래키 Column이 생성되므로, mappedBy 속성은 부모 Entity에서 @OneToMany의 속성으로서 선언됨.

      。양방향 Mapping 시 양쪽 Entity에서 FK Column을 중복생성하지 않도록 방지하는 역할 수행.



    • @ManyToOne
      DB Entity간 양방향 관계 설정 시 자식 Entity에서 1:N 관계를 Mapping할 때 부모 Entity의 외래키를 참조하는 field에 선언하는 JPA Annotation

      @ManyToOne(fetch = FetchType.LAZY) :
      。연관관계의 lazily loaded(지연로딩), eagerly fetched(즉시로딩 , 1:N에서 Default)의 여부를 결정.

      • FetchType.EAGER로 설정 시 동일한 query에서 특정 Entity를 참조하면 Mapping된 Entity도 같이 참조하는 기능 수행.

      • FetchType.LAZY의 경우 동일 query에서 특정 Entity만 참조가능하고 Mapping된 다른 DBEntity를 참조하지 못함.


    • @OneToOne 1:1

    • @ManyToMany N:M :
      。실무에서는 사용하지 않는다.
      1:N or N:1을 통해 중간에 Mapping Table을 구현하여 처리.
  • 게시물 역할의 Entity 생성
    。연관관계의 자식은 FK가 있는쪽으로 설정해야한다.
    • DB의 Table과 Mapping 하도록 @Entity를 정의한 게시물 Entity 생성.
      UserPost Entity 간에 연관관계 Mapping을 부여하며 서로 1:N 관계설정.
      ▶ 자식 Entity ( Post ) instance에 @ManyToOne(fetch=FetchType.LAZY)를 정의하여 User Entity를 참조 시 Mapping된 자식 Post의 Detail을 가져오지 않도록 설정.

      JPAAuto-Configuration을 통해 자식 (Post) Entity를 DB Table로 자동 생성 시 부모 (User) Entity의 PK( Id )를 참조하는 FK( user_id) column이 자동 생성
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import jakarta.persistence.*;
                  // 자식 DB Entity
    @Entity
    public class Post {
        @Id
        @GeneratedValue
        private int id;
        private String description;
        @ManyToOne(fetch= FetchType.LAZY)
        @JsonIgnore // Response할 `JSON data`에서 선언한 데이터를 `HTTP Response`에서 제외
        private User user;
        public int getId() {return id;}
        public void setId(int id) {this.id = id;}
        public String getDescription() {return description;}
        public void setDescription(String description) {this.description = description;}
    }
    • User Entity에는 Post instance들을 저장하는 List를 정의.
      User Entity 기준 자식 Entity Post1:N 관계이므로 @OneToMany(mappedBy="자식 Entity에서 FK로서 mapping되는 Field명")을 field에 선언.
      ▶ 이를 Mapping하는 자식 Entity( Post )의 user field에는 이와 반대인 @ManyToOne을 선언.

      User의 Spring Bean에서 post를 가져오기위해 User Class의 posts field에 대해서 getter와 setter를 정의!

      。각 DB Entity는 각각 서로 REST APIHTTP Response의 일부로 사용.
      User Class에서 해당 API를 GET Request를 할 경우 post field가 HTTP JSON Response에 포함하면 안되며 마찬가지로 Post Class의 User instance를 HTTP JSON Response에 포함하면 안되므로 @JsonIgnore를 선언.

      @JsonIgnore : Response할 JSON data에서 선언한 데이터를 HTTP Response에서 제외.
    	@OneToMany(mappedBy="user")
        @JsonIgnore
        private List<Post> posts;
        public List<Post> getPosts() {return posts;}
        public void setPosts(List<Post> posts) {this.posts = posts;}
    }

    。이때 다음 구문을 application.properties에 입력 시 JPA의 Background 상 SQL query를 콘솔에서 확인 가능.
    JpaRepository method(save, findById 등) 사용 시, Background에서 어떤 query가 생성되는지 확인

    spring.jpa.show-sql=true



    。다음처럼 JPA를 통해 2개의 table이 생성된 sql query를 확인 가능.
    post table에서는 User Entity의 PK인 Id를 참조하는 FK user_id column이 자동생성되있음.
    => 해당 user_id는 게시물과 특정 사용자를 연결 시 활용됨.

    • data.sql을 통해 Post DB Table에 초기값 data 정의.
    insert into post(id,user_id,description)
    values(20001,1,'I want to learn AWS'),
          (20002,2,'I want to learn DevOps'),
          (20003,3,'I want to learn Certificate'),
          (20004,4,'I want to learn MultiCloud');

    user_idUserid를 참조하기위해 JPA에 의해 자동생성된 외래키이므로 해당 id의 domain의 값 범위에 있어야한다.


    。초기 Data가 성공적으로 들어가있으며 외래키로서도 정의되어 사용이 가능.

특정 User의 모든 Post와 상호작용하는 API를 Controller Method로 구현
。기존의 User Spring Bean 관련 API를 Spring Data JPA를 이용해 관리하는 Controller Class( = UserResource)에 Post Spring Bean 관련 API를 생성하여 함께 관리되도록 설정.

  • 특정 User instance에 Mapping된 모든 Post instance를 가져오는 API를 Controller Method로 구현
    GET /users/{id}/posts
    HTTP Request를 통해 Mapping된 URL에 User의 id를 포함하여 Application으로 전송 시 @PathVariable을 통해 매개변수에 User instance의 Id를 가져온 후 해당 User instance와 Mapping된 List<Post> posts instance를 return 하여 Post Detail을 가져오기.
// 특정 User와 Mapping된 Post 가져오기
    @GetMapping(path="/jpa/users/{id}/posts")
    public List<Post> GetPostsByUserId(@PathVariable int id){
        Optional<User> user = userRepository.findById(id);
        if (user.isEmpty()){
            throw new UserNotFoundException("id :" + id);
        }else{
            // 해당 User instance에 Mapping된 List<Post> posts instance를 return.
            return user.get().getPosts();
        }
    }

Optional객체.isEmpty()를 활용하기 위해 Repository객체.findById(id).get()으로 Spring Bean을 직접 가져오는게 아닌 userRepository.findById(id)으로 Optional객체를 return.

Id = 1에 해당하는 User Spring Bean에 List<Post> 객체를 return하여 FK로서 Mapping된 모든 Post instance를 가져오게됨.
▶ 해당 List<Post> 객체가 포함하는 Post instance는 연관관계 Mapping이 설정되어 FK user_id=1Post instance를 포함.

。해당 List<Post> 객체는 User Spring Bean을 HTTP Response할때 @JsonIgnore에 의해 포함되지않는다.

  • 특정 User의 특정 Post를 가져오는 API를 Controller Method로 구현
    GET /users/{userid}/posts/{postid}
    HTTP Request를 통해 Mapping된 URL에 특정 User의 id와 Post의 ID를 포함하여 Application으로 전송 시 @PathVariable으로 매개변수로 가져온 후 해당 ID에 해당하는 UserPost 불러오기.
// 특정 User와 Mapping된 특정 Post 가져오기
    @GetMapping(path="/jpa/users/{userid}/posts/{postid}")
    public List<Post> GetPostsByUserIdAndPostId(@PathVariable int userid,@PathVariable int postid){
        Optional<User> user = userRepository.findById(userid);
        if (user.isEmpty()){
            throw new UserNotFoundException("id :" + userid);
        }else{
            List<Post> posts = user.get().getPosts();
            posts.removeIf(post -> post.getId() != postid);
            return posts;
        }
    }


localhost:8080/jpa/users/1/posts/20001 입력 시 다음처럼 정상적으로 id=1 User에 속하는 id=20001Post가 도출됨.

  • 특정 User의 Post를 생성하는 API 생성하기
    POST /users/{id}/posts
    • JpaRepository<Entity,ID Field>를 상속하는 Interface 구축
      Post을 저장하기위한 Repository를 구축하기위한 용도
      User관리를 위한 UserRepository Interface와 동일한 용도.
    import org.springframework.data.jpa.repository.JpaRepository;
    public interface PostRepository extends JpaRepository<Post, Integer> { }
    • POST를 통해 Post를 생성하는 Controller method 구현
      PostRepository instance를 생성 및 생성자 의존성주입.
    UserRepository userRepository;
        PostRepository postRepository;
        // @Autowired  Spring Context가 생성자 기반 Denpendency Injection을 수행.
        public UserResource(UserRepository userRepository, PostRepository postRepository) {
        this.userRepository = userRepository;
        this.postRepository = postRepository;
        }

    Post Spring Bean의 User user instance를 참조하기 위해서 Getter , Setter 를 정의.

        @ManyToOne(fetch= FetchType.LAZY)
        @JsonIgnore
        private User user;
        public User getUser() { return user; }
        public void setUser(User user) { this.user = user; }

    。특정 id의 User instance를 찾은 후 생성할 Post Spring Bean의 User user에 해당 User instance를 설정.

    @Valid : POST HTTP Request Body와 Data Binding 전 유효성검증
    @RequestBody : POST HTTP Request Body의 Data를 Data Binding하여 매개변수의 Java 객체로 변환하여 저장.
    @PathVariable : URL의 PathVariable을 매개변수의 변수로 생성.
    ResponseEntity 관련

    // 특정 User에 Mapping된 Post 생성
        @PostMapping(path="/jpa/users/{id}/posts")
        public ResponseEntity<Post> CreatePost(@PathVariable int id, @Valid @RequestBody Post post){
            // 특정 User instance 조회
            Optional<User> user = userRepository.findById(id);
            if (user.isEmpty()){
                throw new UserNotFoundException("id :" + id);
            }else{
                // HTTP Request Body와 Data Binding된 Post instance의 User field를
                // 상단에서 불러온 User instance로 설정.
                post.setUser(user.get());
                // 이후 해당 Post instance를 DB Table에 저장.
                Post savedPost = postRepository.save(post);
                // ResponseEntity를 이용한 REST API에서 알맞은 Response Status 반환하기
                URI locationHeader = ServletUriComponentsBuilder.fromCurrentRequest()
                        .path("/{id}").buildAndExpand(savedPost.getId()).toUri();
                return ResponseEntity.created(locationHeader).build();
            }
        }

    。URL에 포함된 ID에 해당하는 User instance를 DB Table에서 조회.
    。이후 Client가 POST HTTP Request Body를 전송 시 @RequestBody를 통해 Post instance로 DataBinding한 후
    해당 instance의 User user field를 사전에 조회한 User instance를 setter로 설정 후 JpaRepository.save(post객체)로 DB Table에 저장.
    ▶ 최종적으로 해당 User instance를 가지는 Post instance가 생성됨.
    ▶ Client는 POST Method를 통해 JSON Format으로 data를 입력하여 해당 API를 통해 HTTP Request Message를 전달.

    Location Header, ResponseEntity를 활용해 Application에서 Spring Bean( Post ) Instance 생성되었는지 알려주는 기능 추가.

    API Tester를 이용해 HTTP Request Body를 작성하여 해당 API로 POST 전.
    Location Header를 통해 Client에게 Post가 해당 id로 정상적으로 생성되었음을 지시.

    201 : Created : POST로 성공적으로 새로운 Resource가 생성됨.


    。최종적으로 DB의 Post Table에 해당 URL의 PathVariable를 통해 얻은 User instance의 id를 foreign key로서 생성된 user_id열로 가지는 Post instance가 생성되며 POST의 primary key인 id는 @GeneratedValue에 의해 자동 생성.

    。GET으로 http://loca lhost:8080/jpa/users/10001/posts 검색시에도 해당 post의 대한 정보가 추가되어있음.

    • JpaRepository method를 통해 Background에서 발생하는 SQL Query 분석하기
      spring.jpa.show-sql=true를 통해 JpaRepository객체.save()같은 method에 해당하는 SQL을 Console에서 확인이 가능.
      。Client가 Application에게 HTTP Request Method를 포함하여 HTTP Request Message를 전송 시 URL과 HTTP Request Method에 Mapping된 Controller Method를 실행 및 해당 Method 내부의 JpaRepository method의 SQL 구문을 확인.

      。console에 나온 log로 분석하면, @PathVariable int id를 활용하여 select문을 통해 특정 user 객체가 생성됨.
    Hibernate: select u1_0.id,u1_0.birth_date,u1_0.name from user_details u1_0 where u1_0.id=?
      Optional<User> user = userRepository.findById(id);

    @RequestBody Post post를 통해 생성된 post객체의 user field를 위 과정에서 생성된 user 객체로 설정 후 insert문을 통해 post에 저장.

      Hibernate: insert into post (description,user_id,id) values (?,?,?)
      postRepository.save(post);

    postRepositoryJpaRepository<Post,Integer>를 상속하므로, 해당 Post Bean의 인자를 통해 @Entity로 생성된 DB의 post table에 insert를 통해 저장됨.



  • 특정 Post를 삭제하는 메소드 구현하기.
@DeleteMapping(path="/jpa/posts/{id}")
    public void DeletePostById(@PathVariable int id){
        postRepository.deleteById(id);
    }


Postgres DB와 Spring Boot 연결하기 참고

。영속성 DB와 연결하는것이므로 H2와 같은 In-Memory DB처럼 Application을 재시작하면 Data가 초기화되는것이 아니기 때문에 초기값 설정용 data.sql , schema.sql을 정의 안해도된다.
DB 사용 시 해당 .sql파일을 만들 필요는 없다.

H2 DBSpring JPA를 통해 구현한 DB Entity의 구현내용을 재활용하여 PostgreSQL DB에도 적용이 가능하다.
▶ 。Spring JDBC , Spring JPA를 사용할 경우 PostgreSQL DB , MySQL DB , H2-DB 등에 관계없이 구축된 DB Entity을 매우 쉽게 적용할 수 있다.
Spring JDBCJdbcTemplate Class를 이용하여 SQL을 하드코딩하여 PostgreSQL DB와 상호작용을 수행할 수 있다.

  • Spring Boot에 PostgreSQL DB 연결 시 필요한 dependency
    • Maven
      pom.xml에 정의

      Spring Web

      <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      Spring JDBC

      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jdbc</artifactId>
      </dependency>

      Spring Data JPA

      <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency> 

      PostgreSQL Driver

      <dependency>
                <groupId>org.postgresql</groupId>
                <artifactId>postgresql</artifactId>
                <scope>runtime</scope>
      </dependency>


    • Gradle
      build.gradle에 정의
      implementation 'org.springframework.boot:spring-boot-starter-web' // Spring Web 
      implementation 'org.springframework.boot:spring-boot-starter-jdbc' // Spring JDBC
      implementation 'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA
      implementation group: 'org.postgresql' , name: 'postgresql' , version: '42.2.23'  // PostgreSQL Driver
      runtimeOnly 'org.postgresql:postgresql' 


  • JDBC를 통해 application.propertiesPostgreSQL DB DataSource 정의
    Spring Boot가 기본적으로 자동으로 연결할 PostgreSQL DBDataSource를 설정하기위해 application.propertiespostgresql에 관한 JDBC를 정의.
    @Bean을 선언하여 직접 DataSource instance를 생성하는 방법은 아래에서 소개.
    JdbcUserDetailsManager(DataSource객체)에서 사용됨.

    Spring Boot가 자동으로 연결할 DataSource를 설정 시 application.properties에 기존에 정의된 h2-db 설정을 삭제해야한다.
spring.datasource.url=jdbc:postgresql://주소:포트번호/DB이름
spring.datasource.username=DB계정이름
spring.datasource.password=DB계정비밀번호
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update

    • spring.datasource.url=JDBC URL :
      。 특정 DB의 JDBC URL
      ▶ Local에서 실행되고 있는 PostgreSQL DB의 URL을 정의.
      H2-DB에서 동적 URL을 고정하기 위한 정적 JDBC URL 설정 시에도 사용됨.
      spring.datasource.url=jdbc:h2:mem:testdb

    • spring.datasource.username , spring.datasource.password :
      DB에 접근하기 위한 사용자 이름과 비밀번호

    • spring.jpa.database-platform :
      Hibernate가 사용할 PostgreSQL DB의 Dialect

    • spring.jpa.show-sql :
      SQL Query를 Console에 출력할지의 여부

    • spring.jpa.hibernate.ddl-auto :
      。Application 실행 시 Hibernate가 DB Schema를 자동 Update할지의 여부를 설정.
      spring.jpa.hibernate.ddl-auto=update로 설정할 경우, Entity Class와 DB Table Schema를 동기화.
      • Hibernate
        JPA를 구현한 대표적인 오픈소스.
        Hibernate JAR을 class path에 추가해서 Hibernate를 JPA 구현체로 사용.

      • Dialect :
        JPA 또는 Hibernate에서 사용하는 특정 DB에 해당하는 SQL문법과 기능을 정의한 설정.
        ▶ 각 DB( MySQL , PostgreSQL , Oracle )등은 공통적으로 SQL 표준을 따르지만, 각각 조금씩 차이가 발생하는 SQL 문법을 가지므로 Hibernate는 해당 문법/기능적 차이를 Dialect를 통해 자동으로 조정.

        주요 Dialect

        • org.hibernate.dialect.MySQLDialect : MySQL
        • org.hibernate.dialect.MariaDBDialect : MariaDB
        • org.hibernate.dialect.PostgreSQLDialect : PostgresDB
        • org.hibernate.dialect.H2Dialect : H2DB


  • Spring JDBCPostgreSQL DB DataSource instance 생성하기
    PostgreSQL에서 JdbcUserDetailsManager(DataSource객체)을 통한 DB에 사용자 자격증명을 추가할때의 용도로 활용할 DataSource instance 정의하기. JdbcUserDetailsManager 설정
    ▶ 사용자 자격증명 저장이 필요없는 경우, 설정안해도 application.properties에서 Spring Boot에 자동연결설정된 DataSource를 활용하여 기본적으로 CRUD를 수행 가능.
    • DataSource 생성하는 @Bean Method 정의
      @Configuration이 선언된 Configuration Class에서 DataSource instance를 반환하는 @Bean Method를 생성.
    	@Bean
        public DataSource pgdataSource() {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl("jdbc:postgresql://localhost:5432/GeoDB");
            config.setUsername("postgres");
            config.setPassword("wjd747");
            return new HikariDataSource(config);
        }

    postgresql DataSource를 정의하기위해 application.properties에서 작성된 JDBC 내용과 유사.

    • DataSource : javax.sql.DataSource
      JDBC에서 특정 DBDB Connection을 관리하는 Interface.
      ApplicationDB에 접근 시 DataSource를 통해 Connection을 관리.

      。최적화된 DB Connection Pool을 제공하고, Spring Boot에서 Auto-Configuration이 가능.

    • HikariCP :
      。Spring Boot에서 기본적으로 활용하는 고성능 JDBC Connection Pool을 관리하는 DataSource Interface를 구현한 Class.
      new HikariConfig()를 통해 DB Connection이 구현된 DataSource instance 생성.

      JDBC Connection Pool을 통해 DriverManagerDataSource와 달리 DB Connection을 재사용.
      • Connection Pool :
        DB Connection을 매번 생성 시 성능 저하 발생하는 단점을 보완.
        Connection Pool을 사용 시 일정 수의 DB Connection을 사전에 생성 및 재사용 가능하여 불필요한 DB Connection 생성을 방지 가능.

      • DriverManagerDataSource :
        Spring에서 JDBC Connection을 위한 DataSource Interface를 구현한 Class.
        new DriverManagerDataSource()를 통해 DB Connection이 구현된 DataSource instance 생성.

        。 매번 새로운 DB Connection을 생성하며 연결을 재사용하지 않고 사용하므로 HikariCP에 대체됨.


  • Spring JPA 기능을 활용하여 PostgreSQL과 연동
    Spring JPA를 활용해 REST API를 통해 PostgreSQL DB와 상호작용 수행 시 활용.
    PostgreSQL DB의 Table과 Mapping된 JPA Entity Class 생성
    H2 DBSpring JPA를 통해 구현한 DB Entity의 구현내용을 재활용하여 PostgreSQL DB에도 적용이 가능하다.
    • DB Entity 생성
      PostgreSQL DB와 Data Binding되어 상호작용을 수행하는 Entity Class 생성
      @Entity를 선언하여 Entity Class 생성 및 @Id@GeneratedValueprimary key field에 선언
      ▶ 기존 H2-DB로 실습한 Entity Class를 그대로 활용.
      // PostgresUser.java
    import com.fasterxml.jackson.annotation.JsonIgnore;
    import com.fasterxml.jackson.annotation.JsonProperty;
    import jakarta.persistence.*;
    import jakarta.validation.constraints.Past;
    import jakarta.validation.constraints.Size;
    import java.time.LocalDate;
    import java.util.List;
    @Entity
    public class PostgresUser {
        @Id
        @GeneratedValue
        Integer id;
        @JsonProperty("username")
        @Size(min=2, message = "Name should have at least 2 characters.")
        String name;
        @JsonProperty("birth_date")
        @Past(message="Birth Date should be in the past.")
        LocalDate birthDate;
        public List<PostgresPost> getPosts() { return posts; }
        public void setPosts(List<PostgresPost> posts) { this.posts = posts; }
        @OneToMany(mappedBy="user")
        @JsonIgnore
        private List<PostgresPost> posts;
        public PostgresUser(){}
        public PostgresUser(Integer id, String name, LocalDate birthDate) {
            this.id = id;
            this.name = name;
            this.birthDate = birthDate;
        }
        public Integer getId() { return id; }
        public String getName() { return name; }
        public LocalDate getBirthDate() { return birthDate; }
        public void setId(Integer id) { this.id = id; }
        public void setName(String name) { this.name = name; }
        public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; }
    }

    id field의 경우 Wrapper ClassInteger type으로 설정.
    int와 달리 Wrapper Class객체 이므로 Null값을 저장할 수 있다.
    DB에서 pk가 없거나 아직 할당되지 않은 상태를 지시하기 위해 NULL을 사용할 수 있게 설정.
    Integer vs int

    intInteger 차이
    Java에서 둘다 정수를 다루지만, 기본 자료형(primitive type)과 wrapper class라는 차이가 존재.

    • int
      。기본자료형 ( primitive type )
      null값을 가질 수 없다.

    • Integer
      java.lang.Integer Class 객체
      int를 객체로 감싼 Wrapper Class 객체
      new 키워드로 선언하여 객체 생성.

      null값을 저장 가능.
      DB Entityprimary key로 활용.


    • JPACRUD를 수행하기위한 JpaRepository<Entity,ID Field> Interface 정의
    import org.springframework.data.jpa.repository.JpaRepository;
    public interface PgRepository extends JpaRepository<PostgresUser,Integer> { }

    .

    • JPABusiness Logic을 구현하기위한 Service Class 정의
      Business Logic 구현을 위한 Spring Bean이므로 @Service 선언.
      @Entity에서 명시적 구체화.

      JpaRepository<> interface의 instance 생성 및 생성자주입을 수행하여 CRUD method를 구현
    import java.util.List;
    import java.util.Optional;
    import org.springframework.stereotype.Service;
    @Service
    public class pgService {
        // JpaRepository instance
        PgRepository pgRepository;
        // @Autowired 생략 가능한 Constructor Based Dependency Injection
        public pgService(PgRepository pgRepository) {
            this.pgRepository = pgRepository;
        }
        // Create
        public PostgresUser CreatePGUser(PostgresUser user) {
            return pgRepository.save(user);
        }
        // Update
        public PostgresUser UpdatePGUser(PostgresUser user) {
            return pgRepository.save(user);
        }
        // Read
        public Optional<PostgresUser> GetPGUser(Integer id) {
            return pgRepository.findById(id);
        }
        public List<PostgresUser> GetAllPGUsers() {
            return pgRepository.findAll();
        }
        // Delete
        public void DeletePGUser(Integer id) {
            pgRepository.deleteById(id);
        }
    }

    JpaRepository.save(DB Entity)CREATEUPDATE 기능을 수행.
    ▶ 기존 Entity를 사용 시 UPDATE, 새로운 Entity를 사용 시 INSERT

    • CRUD기능을 포함한 REST API를 구현할 Controller Class 생성
      JPA CRUD를 사전에 정의한 pgService Spring Bean instance를 생성하여 의존성 주입하여 수행.
    import jakarta.validation.Valid;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
    import java.net.URI;
    import java.util.List;
    import java.util.Optional;
    @RestController
    public class PgController {
        // JPA Business Logic을 사전에 정의한 Spring Bean
        pgService pgservice;
        // @Autowired 생략 가능한 Constructor Based Dependency Injection
        public PgController(pgService pgservice) {
            this.pgservice = pgservice;
        }
        // GET Method 구현
        // GET /users : 모든 사용자 조회
        @GetMapping(path="/pg/jpa/users")
        public List<PostgresUser> ListAllUsers(){
            // JpaRepository를 구현한 Service Class의 JpaRepository객체.findAll() Method 활용.
            List<PostgresUser> users = pgservice.GetAllPGUsers();
            return users;
        }
        // GET /users/{id} : 특정 사용자 조회
        @GetMapping(path="/pg/jpa/users/{id}")
        public Optional<PostgresUser> GetUserById(@PathVariable int id){
            // JpaRepository를 구현한 Service Class의 JpaRepository객체.findById() Method 활용.
            // 가져온 데이터는 Optional<PostgresUser>로 return.
            Optional<PostgresUser> user = pgservice.GetPGUser(id);
            if(user.isEmpty()){ // 해당 id가 존재하지 않는 경우 발생.
                throw new UserNotFoundException("id :" + id);
            }
            return user;
        }
        // POST Method 구현
        // POST /users : 새로운 사용자 생성
        @PostMapping(path="/pg/jpa/users")
        public ResponseEntity<PostgresUser> createUser(@Valid @RequestBody PostgresUser user) {
            // @RequestBody : HttpRequest의 Body(=JSON Format)를 Bean으로 변환하여 저장.
            // JpaRepository를 구현한 Service Class의 JpaRepository객체.save() Method 활용.
            PostgresUser savedUser = pgservice.SavePGUser(user);
            //REST API에서 알맞은 Response Status를 Client에게 반환하는 코드
            URI locationHeader = ServletUriComponentsBuilder.fromCurrentRequest()
                    .path("/{id}").buildAndExpand(savedUser.getId()).toUri();
            // 201 : created status에 해당하는 ResponseEntity<User> 객체를 생성하여 반환.
            return ResponseEntity.created(locationHeader).build();
        }
        // Post의 Update를 수행하고 Save한 PostgresPost instance를 return.
        // PUT /users/{id}
        @PutMapping(path="/pg/jpa/users/{id}")
        public PostgresUser UpdateUserById(@PathVariable int id, @RequestBody PostgresUser user){
            // HTTP Request Body가 @RequestBody를 통해 변수에 Mapping되어 Update를 수행.
            pgservice.UpdatePGUser(user);
            return user;
        }
        @DeleteMapping(path="/pg/jpa/users/{id}")
        public void DeleteUserById(@PathVariable int id){
            // JpaRepository를 구현한 Service Class의 JpaRepository객체.deleteById() Method 활용.
            pgservice.DeletePGUser(id);
        }
    }



    PostgreSQL DB에 해당 Entity Class의 Table 명이 없을 경우 Spring JPA에 의해 자동생성됨.



  • Spring JDBC 기능을 활용하여 PostgreSQL과 연동 Spring JDBC
    SQL을 조작하여 PostgreSQL DB과 상호작용시 사용.
    • Spring JDBC를 통해 data의 삽입을 용이하게 하는 Spring Bean Class 생성
      。해당 Class를 통해 Spring Bean instance를 생성하여 Spring JDBC를 통해 DB에 Insert를 수행.
    import java.time.LocalDate;
    public class PostgresUserforJDBC {
        int id;
        String name;
        LocalDate birth_date;
        public PostgresUserforJDBC(int id, String name, LocalDate birth_date) {
            this.id = id;
            this.name = name;
            this.birth_date = birth_date;
        }
        public int getId() {return id; }
        public void setId(int id) {this.id = id;}
        public String getName() {return name;}
        public void setName(String name) {this.name = name;}
        public LocalDate getBirth_date() {return birth_date;}
        public void setBirth_date(LocalDate birth_date) {this.birth_date = birth_date; }
    }
    • Spring JDBC을 활용하여 DAO를 구현하는 Class 생성
      Spring JDBC를 통해 하드코딩한 SQL를 DB에 반영하는 Class객체에 @Repository를 선언하여 Spring Bean으로 구체화.
      。특정 SQL문을 JdbcTemplate Class의 instance를 활용하여 JdbcTemplate객체.update(SQL구문)로 DB에 SQL을 반영하는 Logic을 구현.
      DAO(Data Access Obejct : DB에 접근하는 역할을 수행하는 객체)
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    @Repository
    public class pgJdbcRepository {
        private JdbcTemplate jdbcTemplate;
        // @Autowired  : 생성자기반 의존성주입
        public pgJdbcRepository(JdbcTemplate jdbcTemplate) {
            this.jdbcTemplate = jdbcTemplate;
        }
        // text-block을 활용한 sql 문자열
        private static String sqlinsert =
                """
                insert into postgres_user(id,birth_date,name) values(?,?,?);
                """;
        public void insert(PostgresUserforJDBC user){
            jdbcTemplate.update(sqlinsert,user.getId(),user.getBirth_date(),user.getName());
        }
    }
    • CommandLineRunner 상속 Class 생성
      CommandLineRunner를 상속한 Class를 정의하여 run()을 통해 application 구동 시 DB와 상호작용하는 기능을 구현한 Spring Bean( = pgJdbcRepositor ) instance의 method를 작동.
      JdbcTemplateupdate(sql) method로 DB와 상호작용을 수행하는 method를 구현.

      Query문 입력 시 text-block내에 SQL문을 입력.
     import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    import java.time.LocalDate;
    @Component
    public class pgCodeLineRunnerClass implements CommandLineRunner {
        private pgJdbcRepository jdbcRepository;
        // @Autowired  : 생성자기반 의존성주입
        public pgCodeLineRunnerClass(pgJdbcRepository jdbcRepository) {
            this.jdbcRepository = jdbcRepository;
        }
        @Override
        public void run(String... args) throws Exception {
            jdbcRepository.insert(new PostgresUserforJDBC(2, "Lee",LocalDate.now()));
        }
    }

    @SpringBootApplication Class의 Component Scan에 의해 @Component가 선언된 해당 CommandLineRunner Class를 식별하여 run() method를 실행하여 DAO에서 구현된 JdbcTemplateSQLSpring JDBC 기능을 실행.

    。다음처럼 성공적으로 insert into postgres_user(id,birth_date,name) values(?,?,?); SQL 구문을 통해 Data Insert가 완료된것을 확인 가능.

    JDBC / Spring JDBC / JPA / Spring Data JPA 차이

    • JDBC :
      。SQL을 많이 작성하면서 Java 코드가 많다.
      ▶ 하드코딩이 많음.

    • Spring JDBC :
      。SQL을 많이 작성
      JdbcTemplate instance를 통해 Java코드는 적게 사용.

    • JPA :
      。SQL을 작성하지않음
      EntityManager을 통해 DB Entity를 DB Table로 Mapping 하는 과정 필요.

    • Spring JPA :
      SQL , EntityManager을 사용하지 않는다.
      EntityManager 대신 JpaRepository<Entity class, ID> Interface를 상속한 Interface를 Instance로 생성하여 활용.

      Spring JDBC , Spring JPA를 사용할 경우 PostgreSQL DB , MySQL DB , H2-DB 등에 관계없이 구축된 DB Entity을 매우 쉽게 적용할 수 있다.

      Spring JDBCJdbcTemplate객체의 경우 직접 SQL을 작성하여 JdbcTemplate객체.update(SQL)로 상호작용한다면 JpaRepository는 자동으로 Query가 생성되는 Method를 제공.
      • DAO ( Data Access Object ) :
        DB와 직접 상호작용을 수행하는 Object로서 개발자가 SQL을 직접 작성하여 CRUD를 수행.
        Spring에서는 보통 @Repository Annotation으로 선언하여 Spring Bean instance으로 생성.

        JdbcTemplate 같은 Spring JDBC를 활용하여 DB와 상호작용하는 method를 제공하는 Class를 instance로 생성하여 활용.

      • ORM ( Object-Relational Mapping )
        。Application의 DB Entity Class instance를 RDBMS의 DB Table에 자동으로 영속화 하는것을 의미.
        DB Entity instance와 RDBMS DB Table 간 자동 Mapping.

        SQL을 직접 사용하지 않고, DB Entity instance를 중심으로 DB의 Data 조작을 수행.
        RDBMS ( H2-DB, mySQL , postgreSQL ) 의 변경에도 유연한 적용이 가능.

Spring Security로 Authentication 구현

Spring Security Framework는 생성한 모든 API에 대해 전부 보안을 적용할 수 있다.

REST API에 보안 ( 인증:Authentication / 권한:Authorization 등 ) 기능을 담당하는 Spring Security Framework 활용하기.
HTTP Transaction에서 Server의 특정 Resource 접근 시 자격증명을 전달하여 접근하도록 설정.
Spring Security 공부내용

  • Spring Security Dependency 추가하기.
  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
  </dependency>

。 dependency를 추가 후 spring application을 재실행 시 console의 log창에 개발용 자격증명의 PW가 생성되어 표시.
。 Spring Security Framework의 dependency를 추가하는 경우 Spring application의 Controller가 mapping하는 모든 url로 연결 시 Spring 로그인 창이 도출.

API TesterREST APIGET HTTP Request를 전송하여 User Spring Bean instance를 생성 시 Http Status Code:401( Resource에 유효한 자격 증명이 없기 때문에 HTTP Request가 적용되지 않았음 )이 Response됨.
자격증명을 전달하여 API가 동작하도록 설정

Spring Security의 개발용 자격증명

  • Spring Security는 해당 Framework의 Dependency만 정의하더라도 Controller에 의해 Mapping된 URL 접속 시 Spring Security의 기본인증폼을 전송하며, Auto-Configuration에 의해 다음 개발용 자격증명을 자동 생성.
    Console에 표시된 PW는 Application이 재실행될때마다 초기화
    。ID : user
    。PW : console에 표시된 PW

  • 개발용 자격증명의 Username과 Password 변경방법
    application.properties에 다음 구문을 추가.
spring.security.user.name=user123
spring.security.user.password=pw456

。해당 구문 추가 후 로그인 시 변경된 개발용 자격증명에 로그인이 가능.

  • Authorization Header를 추가하여 REST API를 활용하여 GET HTTP Request 전송하기
    Authorization Header에 Username과 Password를 입력하여 API를 전송.


    Spring Security에 의해 Application과 Mapping된 모든 Resource URL이 보호되었으나, Authorization HeaderHTTP Request에 첨부하여 전송 시 성공적으로 Resource가 반환됨.
    • HTTP Authorization Header :
      。Client가 Server에 인증정보를 전달하는 HTTP Header.
      API를 통해 HTTP Request 전달 시 권한( token 등 )을 포함하는 역할을 수행.
      ▶ Server는 Authorization Header를 확인하여 HTTP Request의 허용여부를 결정.


  • API를 통해 POST HTTP RequestAuthorization Header를 첨부하여 전송하여 DB에 객체를 생성하기
    Authorization Header에 자격증명을 첨부하여 API에 POST Http Request로 전송 시 HttpStatusCode:403(서버에 요청이 전달되었지만, 권한 때문에 거절)이 반환.
    Spring Security에서 Default로 설정된 CSRF Protection에 의해 PUT, POST, DELETE Request로 부터 DB를 보호하는 기능이 작동되었으므로.
    CSRF 관련 공부내용

    Filter Chain Customizing
    。승인되지 않은 요청에 대해서는 로그인 양식이 표현하는 기능 구현
    CSRF를 해제해서 POST, PUT, DELETE Request를 보내는 기능 구현.
    Spring Security는 기존의 filter chain을 Override할 경우 Chain 전체를 재정의해야함.
    HttpSecurity를 통해 기본기능과 추가기능을 모두 구현

    • Spring Security 설정 Spring Security 설정 예제
      SpringFilterChain에 관한 전반적인 Configuration Class를 구현.
      ▶ 기본기능과 추가기능을 모두 구현해야함.
      @Configuration을 선언한 class를 생성 및 HttpSecurity instance를 매개변수로하여 SecurityFilterChain type의 Spring Bean instance를 생성 및 return하는 @Bean method를 구현.
      @Bean method에서는 HttpSecurity instance를 이용하여 SecurityFilterChain기본기능( Default )과 추가기능( Additional )을 모두 구현 후 HttpSecurity instance를 반환.
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.Customizer;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.web.SecurityFilterChain;
    @Configuration
    public class pgSecurityConfiguration {
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            // 인증된 Http Request에 대해 승인처리 설정.
            http.authorizeHttpRequests(
                    auth->auth.anyRequest().authenticated()
            );
            // 인증되지않은 Http Request에 대해 로그인 양식 도출.
            http.httpBasic(Customizer.withDefaults());
            // CSRF 비활성화
            http.csrf(csrf->csrf.disable());
            // SecurityFilterChain instance를 Spring bean으로 반환
            return http.build();
        }
    }

    。기본기능 : Authentication 되지 않은 HttpRequest에 대해서 login form 표현

      HttpRequest객체.httpBasic(withDefaults())

    Authorization Header에 자격증명을 담아 HTTP Request를 전달해야하므로, formLogin이 아닌, httpBasic 사용.
    httpBasic()에 관련된 기본값인 withDefaults()을 적용하여 기본 로그인 페이지로 이동하도록 설정.

    HttpSecurity객체.httpBasic(Customizer.withDefaults())
    。Spring Security에서 HTTP Basic 인증을 설정하는 method.
    ▶ 사용자의 ID와 PW를 HTTP RequestHTTP Authorization Header에 포함하여 인증하는 방식.

    HTTP Basic : ID와 PW를 Base64로 인코딩하여 HTTP Authorization Header에 포함하여 Server로 전송.


    Authorization Header에 자격증명을 입력 한 후, JSON Format으로 data를 정의하여 API로 POST HTTP Request를 전달 시, CSRF Protection에 의한 제약 없이 정상적으로 DB에 전달되었음을 확인 가능.
    POST, GET, PUT, DELETE 모두 사용 가능.

    Spring Security의 인증방식
    HttpSecurity객체를 이용하여 Spring Security를 설정

    • formLogin()
      HttpSecurity객체.formLogin() : 로그인 기능 활성화
      。Spring Security에서 제공하는 인증방식.
      。 Application에서 UI를 이용해 Web Login Form으로 로그인 시 활용하며 Session 기반 인증.
      ▶ Application에서 해당 User의 Session 상태가 유효한지 판단하여 처리.

      Custom Login Page를 사용 가능.
      。쉽게 Log-out 가능.
      CSRF Protection , HTTP Session으로 보안을 유지.
      • Form Login 특징
        Client가 Server에 Mapping된 resource(= URL)을 요청 시 해당 URL이 인증이 필요한 경우 Server는 Login page를 반환
        。Client는 ID와 PW를 입력 후 로그인 요청 시 POST method로 data가 Server에 전송.
        。Server는 해당 data를 확인 후 사용자 정보와 Matching할 경우 SessionToken을 생성하여 저장.


    • httpBasic()
      。http 프로토콜에서 정의한 기본인증방식.
      REST API에서 HTTP Request로 Application의 Resource에 접근 시 Authorization Header를 요구.
      REST API에서 매 HTTP Request마다 Authorization Header에 ID와 PW를 포함하여 전송하여 인증하는 방식.
      ▶ 접근인증방식은 브라우저에서 Client가 Username과 Password를 Authorization Header를 통해 전송하면 됨.

      。웹브라우저의 기본 팝업으로 Authorization HeaderID/PW 입력 기능 제공.

번외 : REST API의 저장소를 H2-DB에서 MySQL로 변경하기.

MySQL DBDocker Container로 실행하여 Application을 MySQL DB로 연결.

。H2-DB 관련 dependency 삭제 및 application.properties에서 관련내용 지우기.
JPA & Hibernate 사용 시 사용중인 DB의 교체가 매우 편리
전단계 Docker 관련 공부내용

  • Docker를 이용해서 MySQL 실행
    。Docker를 실행한 후 PowerShell을 실행하여 다음 구문을 입력하여 MySQL에 속한 Docker Container 생성.
docker run --detach --env MYSQL_ROOT_PASSWORD=dummypassword --env MYSQL_USER=social-media-user --env MYSQL_PASSWORD=dummypassword --env MYSQL_DATABASE=social-media-database --name mysql --publish 3306:3306 mysql:8-oracle

。생성이 될 경우 다음 구문으로 실행중인 Container 목록을 확인 가능.

application.properties에 다음 설정 정의 및 H2-DB관련 설정 삭제.
Property 정보

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/social-media-database
spring.datasource.username=social-media-user
spring.datasource.password=dummypassword
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
  • MySQL dependency 추가
    H2-DB 관련 dependency는 삭제하기.
  <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
  </dependency>
  • 이때 , 브라우저에 localhost:8080/jpa/users를 검색 시 Response에서 return되는 List<User Bean>들이 포함되지 않는다.
    。이는 data.sql 내 정의한 초기값 data는 In-Memory DB(H2-DB)에 연결시만 실행되지 실제 DB(MySQL)로 연결 시 실행되지 않기 때문.
  • API Tester을 이용해 POST Method로 User Bean 생성 후 조회하기.
    。POST Method로 HttpRequestBody에 생성할 Bean에 맞는 field 내용을 입력 후 API를 전송하여 각각 User과 해당 User의 Post Bean을 생성.


    。이는 Docker를 통해 MySQL에 저장되므로 Application을 재시작하고, GET Method로 조회 시에도 정상적으로 생성한 Bean data가 response됨!
  • Application과 MySQL이 연동되었는지 확인하기위해 MySQLShell를 이용하여 MySQL DB에 생성된 데이터 조회하기
    • Windows PowerShell을 관리자권한으로 실행후 mysqlsh을 입력.
    • \connect DB사용자이름@localhost:포트번호 입력.
      。DB사용자 이름을 통해 3306 포트로 실행되는 DB로 연결.
      ex) \connect social-media-user@localhost:3306
    • 해당 DB의 MySQL Password를 입력.
      ex) dummypassword
    • \use <schema>으로 해당 DB schema 사용 선언.
      ex) \use social-media-database
    • \sql을 입력 후 SQL Query를 입력하여 데이터 조회.
      ex)
      \sql
      select * from post;



    。다음처럼 생성된 데이터가 정상적으로 조회됨!

    • DB에 생성된 데이터는 In-Memory DB와 다르게 영속성을 가지게되므로, Application을 재실행해도 동일하게 조회가 가능하다!
profile
공부기록 블로그

0개의 댓글