연관관계 매핑 : 비즈니스 로직, 비즈니스 요구사항에 따라개발자가 더 적절한 관계 설정 방법을선택
다중성
다대일(@ManyToOne)
일대다(@OneToMany)
일대일(@OneToOne)
다대다(@ManyToMany)
※ 보통 다대일과 일대다 관계를 많이 사용하고 다대다 관계는 실무에서 거의 사용하지 않는다.
단방향, 양방향
데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블 조인이 가능하다.
데이터베이스는 양방향으로 쿼리가 가능하기 때문에 단방향이니 양방향이니 나눌 필요가 없다.
반면, 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다.
그렇기 때문에 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다.
사실, 엄밀하게는 양방향 관계↔️는 없고 두 객체가 단방향 참조를 각각 가져서 양방향 관계처럼 사용하고 말하는 것
연관관계 주인
두 객체(A, B)가 양방향 관계, 다시 말해 단방향 관계 2개(A→B, B→A)를 맺을 때, 양방향 참조가 존재하기 때문에 어느 쪽에서 외래키를 관리할지 정해야한다.
외래 키를 가진 테이블을 매핑한 엔티티에서 외래 키를 관리하는게 효율적이다.
따라서 이곳을 연관관계의 주인으로 선택한다. 외래 키를 가진 엔티티가 주인이라고 생각하면 쉽다.
일대다, 다대일 관계에서 항상 '다'쪽이 외래키를 가진다. 주인이 아닌 쪽은 외래 키를 변경할 수 없고 읽기만 가능하다.
다대일(N : 1)단방향
게시판(Board)과 게시글(Post)의 관계로 이해해 보기!
요구 사항
하나의 게시판(1)에는 여러 게시글(N)을 작성할 수 있습니다.
하나의 게시글은 하나의 게시판에만 작성할 수 있다.
게시글과 게시판은 다대일 관계를 갖습니다.
데이터베이스를 기준으로 다중성(게시글N : 게시판1)을 결정했다. 즉, 외래 키를 게시글(N)이 관리하는 일반적인 형태.
다대일 단방향에서는 다 쪽인 Post에서@ManyToOne만 추가해준 것을 확인할 수 있다. 반대로 Board에서는 참조하지 않는다. (단방향이기 때문)
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
//... getter, setter
다대일(N : 1)양방향
다대일 양방향으로 만드려면 일(1) 쪽에 @OneToMany 를 추가하고 양방향 매핑을 사용했으니 연관 관계의 주인을 mappedBy 로 지정해준다.mappedBy로 지정할 때 값은 대상이 되는 변수명을 따라 지정하면 된다. 여기서는 Post 객체(대상)의 board라는 이름의 변수이기 때문에 board로 지정
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
@ManyToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "board")
List<Post> posts = new ArrayList<>();
//... getter, setter
}
일대다(1 : N) 단방향
(참고로 실무에서는 일대다(1:N) 단방향은 거의 쓰지 않는다.)
데이터베이스 입장에서는 무조건 다(N)쪽에서 외래키를 관리한다. 일(1)쪽 객체에서 다(N) 쪽 객체를 조작(생성,수정,삭제)하는 방법!
@Entity
public class Post {
@Id @GeneratedValue
@Column(name = "POST_ID")
private Long id;
@Column(name = "TITLE")
private String title;
//... getter, setter
}
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
private String title;
@OneToMany
@JoinColumn(name = "POST_ID") //일대다 단방향을 @JoinColumn필수
List<Post> posts = new ArrayList<>();
//... getter, setter
}
@OneToMany에mappedBy가 없어진다. 양방향이 아니기 때문. 대신@JoinColumn을 이용해서 조인을 한다.
실제 사용은 아래와 같다.
//...
Post post = new Post();
post.setTitle("가입인사");
entityManager.persist(post); // post 저장
Board board = new Board();
board.setTitle("자유게시판");
board.getPosts().add(post);
entityManager.persist(board); // board 저장
//...
위와 같은 시나리오로 동작을 살펴보면, post를 저장할 때는 멀쩡하게 insert 쿼리가 나가지만 그 다음이 문제이다.
board를 저장할 때는 Board를 insert하는 쿼리가 나간 후에 post를 update하는 쿼리가 나간다.
왜냐하면board.getPosts().add(post);부분 때문!
Board 엔티티는 Board 테이블에 매핑되기 때문에 Board 테이블에 직접 지정할 수 있으나, Post 테이블의 FK(BOARD_ID)를 저장할 방법이 없기 때문에 조인 및 업데이트 쿼리를 날려야 하는 문제가 있다.
치명적인 단점
일만 수정한 것 같은데 다른 수정이 생겨 쿼리가 발생하는 것.
Board를 저장했는데 왜 Post가 수정이 되지? 이런 생각을 하게 만듦.
업데이트 쿼리 때문에 성능상 이슈는 그렇게 크지는 않음.
정리 : 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자. 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래 키를 관리한다는 것은 성능 문제도 있지만 관리도 부담스럽다. 해결법은 다대일 양방향 매핑을 사용하는 것이다.
일대다(1 : N) 양방향
(실무 사용 금지 )
일대다 양방향 매핑은 존재하지 않는다. 대신 다대일 양방향 매핑을 사용해야한다. (다대일 양방향과 일대다 양방향은 사실 똑같은 말이다) 양방향 관계에서 @OneToMany는 주인이 될 수 없다. 관계형 데이터베이스 특성상 일대다, 다대일 관계는 항상 '다'쪽에 외래 키가 있다. 따라서 연관관계의 주인은 항상 @ManyToOne이다.
그렇다고 일대다 양방향 매핑이 완전히 불가능하지는 않다. 일대다 단방향 매핑 반대편에 같은 외래 키를 사용하는 다대일 단방행 매핑을 읽기 전용으로 추가하면 된다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
...
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
@OneToMany
@JoinColumn(name = "TEAM_ID") //MEMBER테이블의 TEAM_ID(FK)
private List<Member> members = new ArrayList<Member>();
private String name;
...
}
둘 다 같은 키를 관리하므로 문제가 될 수 있지만 Member의 JoinColumn속성을 보면 읽기만 가능하게 했다. 이 방법은 다대일 양방향처럼 보이게하지만 일대다 단방향 매핑이 가지는 단점을 그대로 가진다.웬만하면 다대일 양방향 매핑을 사용하자.
아래 다이아그램은 이번 주특기 심화주차 레벨2 ERD를 설계한 모델인데, 확실히 ERD를 통해 직관적으로 테이블의 연관관계를 파악할 수 있다.
3. IntelliJ 에서 작성한 ERD 확인 하는법
위와 같이 Show Entity Relationship Diagram 을 클릭하면 아래와 같이 작성한 코드의 테이블 관계를 확인 할 수 있다.
4. [DATABASE] 식별과 비식별 관계
기본키(PK) : 테이블의 하나의 행의 여러 정보들 중 이를 식별해 낼 수 있는 정보
외래키(FK) : 테이블간의 관계(참조하는 테이블과 참조되는 테이블 간의 관계)를 나타내는 정보
외래키를 사용하여 참조하고 참조되는 두 테이블간의 관계에 따라 식별관계와 비식별관계로 나눌 수 있다.
식별관계 : 식별관계는 실선으로 나타내 준다.
상품과 주문의 다대다 관계의 두 테이블이 있다. 상품 테이블에서는 상품번호가 기본키이고 주문테이블은 주문번호가 기본키이다.
상품과 주문테이블 사이에 주문_상품 테이블을 만들어 일대다 관계로 연결하면 아래와 같다.
상품테이블과 주문테이블의 기본키인 상품번호와 주문번호가 주문상품 테이블의 외래키가 되었다.
그리고 이 두 외래키는 주문상품테이블의 정보를 식별할 수 있는 기본키(2개이상의 컬럼도 기본키로 구성될 수 있다.)의 역할도 하게된다. 이러한 관계를 식별관계라고한다.
식별관계에 대해 정리를 해보자면
부모테이블(상품, 주문테이블) 기본키(PK)가 자식 테이블(주문_상품)의 외래키이자 기본키로 사용되는 관계이다.
자식 테이블의 행(정보)를 추가할 때 부모테이블의 참조 행(상품번호 또는 주문번호)이 없다면 자식테이블의 행을 추가 할 수 없다. : 주문_상품테이블은 상품번호와 주문번호 중 하나라도 없다면 기본키를 만들 수 없게 되고(두개의 외래키가 합쳐 기본키가 되므로) 기본키가 없어 정보를 식별할 수 없으므로 데이터를 넣을 수 없다. : 예를 들면 게시판의 작성글과 댓글의 관계를 식별관계라고 할 수 있다.(작성글이 없다면 댓글도 없다)
비식별관계 : 비식별관계는 점선으로 표시한다.
부모 테이블을 참조한 테이블에서 참조된 외래키가 기본키가 아닌 일반속성(컬럼)으로 참조되었을 때를 말한다.
위의 그림의 주문_상품테이블에 정보를 식별할 수 있는 기본키를 추가하면 다음과 같다.
주문상품번호라는 기본키를 추가하였다. 주문상품번호로 주문_상품테이블의 정보들을 식별할 수 있게 되고 외래키(상품번호와 주문번호)는 테이블의 일반속성이 되었다.
자식 테이블의 행(정보)를 추가할 때 부모테이블의 참조 행(상품번호 또는 주문번호)이 없어도 자식테이블의 행을 추가 할 수가 있다. : 예를 들면 회사의 부서와 사원의 관계를 비식별관계라고 할 수 있다. (사원이 부서가 배정되지 않을 수도 있으므로)
여기서 하나 참고할 부분에 대해 말하자면 위의 그림에서 비식별관계는 주문상품번호라는 기본키를 임의로 추가하여 만들었다. 이렇게 추가한 키를 인조키라고 하는데 기본키를 인조키로 설정하는 것이 권장된다. 이유는 식별관계를 나타내는 그림에서 보면 상품번호와 주문번호 두개의 키가 주문상품테이블의 기본키로 되어있다. 기본키는 최대한 변하지 않는 값이어야하는데 어떤이유에서 상품번호 또는 주문번호가 변경이 된다던지 사용을 할 수 없게 된다면 데이터 구조를 다시 만들어야하는 경우가 발생하기 때문이다.
Enables Spring Security’s default configuration, which creates a servletFilteras a bean namedspringSecurityFilterChain. This bean is responsible for all the security (protecting the application URLs, validating submitted username and passwords, redirecting to the login form, and so on) within your application.
Creates aUserDetailsServicebean with a username ofuserand a randomly generated password that is logged to the console.
Registers theFilterwith a bean namedspringSecurityFilterChainwith the Servlet container for every request.
WebSecurityConfig (springboot 2.7이상) 파일로 스프링 시큐리티 활성화 하기
package com.sparta.springsecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf().disable();
http.authorizeRequests().anyRequest().authenticated();
// 로그인 사용
http.formLogin();
return http.build();
}
}
③ CSRF(사이트 간 요청 위조, Cross-site request forgery)
공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것
CSRF 설정이 되어있는 경우 html 에서 CSRF 토큰 값을 넘겨주어야 요청을 수신 가능
쿠키 기반의 취약점을 이용한 공격 이기 때문에 REST 방식의 API에서는 disable 가능
POST 요청마다 처리해 주는 대신 CSRF protection 을 disable
Spring Security 주요 컴포넌트
① Spring Security 와 Filter
Spring Security는 요청이 들어오면 Servlet FilterChain을 자동으로 구성한 후 거치게 한다.
FilterChain은 여러 Filter를 chain형태로 묶어놓은 것을 의미
여기서 Filter 란, 톰캣과 같은 웹 컨테이너에서 관리되는 서블릿의 기술이다.
Filter는 Client 요청이 전달되기 전후의 URL 패턴에 맞는 모든 요청에 필터링을 해준다. CSRF, XSS 등의 보안 검사를 통해 올바른 요청이 아닐 경우 이를 차단해 준다. 따라서 Spring Security는 이런한 기능을 활용하기위해 Filter를 사용하여 인증/인가를 구현하고 있다.
② SecurityFilterChain
session, jwt 등의 인증방식들을 사용하는데에 필요한 설정을 완전히 분리할 수 있는 환경을 제공한다.
③ AbstractAuthenticationProcessingFilter : 사용자의 credential을 인증하기 위한 베이스 Filter
④ UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter 는 AbstractAuthenticationProcessingFilter를 상속한 Filter다.
기본적으로 아래와 같은 Form Login 기반을 사용할 때 username 과 password 확인하여 인증한다.
Form Login 기반은 인증이 필요한 URL 요청이 들어왔을 때 인증이 되지 않았다면 로그인페이지를 반환하는 형태이다.
⑤ SecurityContextHolder
SecurityContextHolder 에는 스프링 시큐리티로 인증을 한 사용자의 상세 정보를 저장한다.
SecurityContext 란? SecurityContextHolder 로 접근할 수 있으며 Authentication 객체를 가지고 있다.
Authentication
현재 인증된 사용자를 나타내며 SecurityContext 에서 가져올 수 있다.
principal : 사용자를 식별한다. Username/Password 방식으로 인증할 때 보통 UserDetails 인스턴스다.
credentials : 주로 비밀번호, 대부분 사용자 인증에 사용하고 다음 비운다.
authorities : 사용자에게 부여한 권한을 GrantedAuthority 로 추상화하여 사용한다.
UsernamePasswordAuthenticationToken는 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, 인증객체를 만드는데 사용된다.
UserDetailsService : username/password 인증방식을 사용할 때 사용자를 조회하고 검증한 후 UserDetails를 반환한다. Custom하여 Bean으로 등록 후 사용 가능하다.
검증된 UserDetails : UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용되며 해당 인증객체는 SecurityContextHolder에 세팅된다. Custom하여 사용가능하다.