왜 갑자기 외래키..?

더보기

데이터베이스 수업을 듣기전 JPA로 프로젝트 하고 SQLD 자격증 공부는 수개월 후에 따로 하다보니 관련 개념을 너무 주먹구구식으로 알고 있었다. 정리해보자!

1. 외래키는 왜 쓰는가?

외래키를 생각해보기 전에, 키는 무엇인가? 테이블에서 데이터를 구분할 수 있는

여러 글을 찾아봤는데, 외래키를 사용하는 이유는 두 가지로 대표할 수 있을 것 같다.

1.1. 중복 데이터 관리

ㅇ기본키와 외래키가 하는 역할

키는 테이블에 있는 레코드들을 식별할 수 있도록 한다.

키만 있다면 유일한 레코드를 뽑아낼 수 있다.

기본키는 데이터를 식별해야 하는 문제를 해결한다.

외래키는 다른 테이블 데이터를 식별해야 하는 문제를 해결한다.

 

자기 테이블 데이터를 식별해야 하는 이유는 알겠는데, 왜 다른 테이블 데이터를 식별해야 하는가?

 

다른 테이블에는 그 테이블의 기본키가 있을 것 아닌가?

 

외래키가 필요한 이유

 

위와 같은 컬림을 가진 테이블을 이용해 사용자가 웹사이트에서 물건을 주문하는 상황을 생각해보자.

 

이 사이트의 유일한 단골인 한 사용자가 두 달동안 물건을 1000개 샀다. 컬럼 a, b, c, d는 같은 사용자에 대한 정보가 들어갈 것이고 기껏해야 e, f, g, h 컬럼 내용만 달라질 것이다. 같은 정보를 반복해서 저장하는 것은 메모리 낭비다.

 

이런 경우에는 노란색 영역을 사용자 테이블, 빨간색 영역을 주문 테이블로 나누고 user_id인 컬럼 a를 외래키로 둬야 한다. 

 

1.2. 참조 무결성 원칙

외래키를 사용하는 두번째 이유는 참조 무결성 원칙을 지키기 위해서이다.

 

참조 무결성 원칙은 기본키가 지정된 테이블과 외래키가 지정된 테이블 사이에서, 외래키로 지정된 컬럼 데이터가 기본키로 지정된 테이블 컬럼값 외 값을 가질 수 없다는 제약 조건이다.

 

만약 위 예시에서 사이트 단골 사용자가 회원 탈퇴를 한다고 생각해보자.

 

외래키가 걸려있지 않다면 사용자는 탈퇴했으나 주문 테이블에서 주문자 정보는 남아있을 것이고 수동으로 탈퇴한 회원에 대해 적합한 처리("탈퇴한 회원입니다" 메세지를 띄우는 등)를 하는 데에 관련 유지 비용이 지속적으로 들 것이다.

 

반면 외래키를 건다면 회원 삭제 연산이 이뤄질 때, 연관된 주문 테이블에서 삭제된 회원이 주문한 정보를 어떻게 처리할 것인지 규칙을 걸어 처리할 수 있다.

 

외래키는 여러 테이블 간 데이터가 무결하다는 것을 보장해준다.

2. 외래키 없이 Join할 수 있는가?

Join 연산은 나누어진 앞서 살펴본 사용자, 주문 테이블처럼 데이터 중복 방지를 위해 나누어진 두 테이블을 연결해 원하는 데이터를 찾기 위해 필요하다.

 

Join 연산에는 보통 기본키와 외래키를 사용하도록 언급해서 나는 Join으로 두 테이블을 연결해서 데이터를 뽑아내기 위해 기본키, 외래키를 사용하는 줄 잘못 알고 있었다.

 

Join 연산에 키는 필수가 아니다. 일반 컬럼으로도 Join할 수 있다.

3. 기존 테이블에 외래키를 도입할 수 있는가?

외래키 없이 Join이 가능하다. 따라서 외래키를 설정하지 않은 데이터베이스도 있을 것이다. 

 

아래 링크를 참고하면 이유는 다양해 보인다.

 

1. 성능이 느리다.

-> 성능 오버헤드 이슈가 있긴 하지만 현재는 무시할만한 수준이다.

-> 외래키가 속도에 영향을 미치기 때문에 도입하지 않는다는 말에는 모순이 있다. 물론 외래키를 걸고 무결성을 확인하면 추가 연산이 필요하므로 느려지겠지만 외래키를 걸지 않는다고 생각해보자. 프로그램 수준에서 데이터 무결성을 확인하기 위해 사전에 select 연산을 할텐데 이 연산이 외래키를 거는 것보다 빠르다고 할 수 있을까?

->  단, 성능이 매우아주많이 중요한 증권거래시스템 같은 경우에는 외래키를 도입하지 않을 수도 있다. 

 

2. 기존 데이터가 이미 무결성에 맞지 않아서 외래키를 걸 수 없다.

-> enable novalidate옵션으로 기존 데이터 무결성을 검증하지 않도록 설정하면 된다.

 

3. 테스트 데이터를 생성할 때 참조 무결성 때문에 불편하다.

-> 개발기간 중에 외래키를 Disable 시키면 된다.

 

4. 트랜잭션 내에서 부모 테이블과 자식 테이블 관계에 따라 연산 순서가 달라져야 할 수 있다.

-> 외래키를 deferred 옵션으로 만들어 매 문장마다가 아닌, 커밋 시점마다 정합성 검사를 한다.

 

요약하자면, 속도에 민감한 서비스가 아니라면 데이터 품질을 보장받기 위해 외래키를 사용하는 것이 좋다.

 

다만 데이터 무결성을 지키려면 개발 과정이 귀찮아지는데 제일 나아 보이는 해결 방법은 외래키를 deferred로 만들고 disable 시킨 후 개발하는 것이다.

 

https://engineering-skcc.github.io/oracle%20tuning/foreign_key_%EC%97%86%EC%9D%B4_%EA%B5%AC%EC%B6%95%ED%95%98%EB%8A%94_DB/

 

Foreign Key 없이 구축하는 관계형 데이터베이스 시스템에 대한 생각

Foreign Key (Referential Integrity) 없이 구축하는 관계형 데이터베이스 시스템에 대한 생각

engineering-skcc.github.io

 

 

기존 테이블에 외래키를 도입할 때 한 가지 주의할 점은 s-lock에 따른 DeadLock 가능성이다.

 

https://m.blog.naver.com/parkjy76/220639066476

 

자바 데이터 타입은 크게 두 가지로 나뉜다.

 

1. 기본 타입(Primitive Type)

자바 기본 타입은 총 8가지로, 메모리 위치에 정수, 실수, 논리 값을 직접 저장한다. 런타임 데이터 영역에서 stack 영역에 생성되고 기본 타입을 사용할 때는 메모리 사용 크기에 주의해 표현할 수 있는 수를 벗어난 값을 저장하지 않도록 한다. 값의 종류는 다음과 같다. 

  • 정수 값 (일반적으로 n bit 메모리를 사용한다면 -2^(n-1) ~ (2^(n-1))-1 사이에서 값을 저장할 수 있다.)
    • byte: 8bit
    • char: 16bit (유니코드 포함)
    • short: 16bit
    • int: 32bit
    • long: 64bit
  • 실수 값
    • float: 32bit
    • double: 64bit
  • 논리 값
    • boolean: 8bit

 

2. 참조 타입(Reference Type)

자바 참조 타입은 크게 4가지며, 메모리 위치에 갖고자 하는 값이 저장된 주소를 저장한다. 참조 타입에서 참고하고자 하는 실제 값은 기본 타입과 달리 heap 영역에 생성되므로 참조되지 않는 변수는 garbage collection에 의해 삭제되기도 한다. 참조 타입 값을 비교하기 위해서는 주소를 비교하게 되는 == 연산자가 아닌, equals() 메서드를 사용해야 한다.

  • 배열
  • enum
  • 문자열
  • 클래스
  • 인터페이스

 

3. 자바에서 int, Integer는 뭐가 다른가?

int는 기본 타입이고 Integer는 Wrapper class, 즉 참조 타입이다. 사용하려는 용도에 따라 두 타입 중 하나를 선택할 수 있다.  parseInt()는 String을 int로 반환하고 valueOf()는 String을 Integer 클래스로 반환한다.

  int Integer
데이터 타입 기본 타입 참조 타입 (Wrapper Class)
특징 산술 연산이 가능하지만, null 초기화 불가 boxing 상태에서는 산술 연산이 불가하지만, null 처리가 용이해 SQL 연동 시 사용한다.

Boxing, unboxing 자료형 변환하기

// boxing: primitive Type -> reference Type
Integer referenceInt = new Integer(primitiveInt);

// unboxing: reference Type -> primitive Type
int primitiveInt = referenceInt.intValue();

// Auto boxing: primitive Type -> reference Type
int primitiveInt1 = 1;
Integer referenceInt = primitiveInt;

// Auto unboxing: reference Type -> primitive Type
int primitiveInt2 = referenceInt;

디버그 모드 왜 쓰는가?

api를 실행했을 때 예상과 다르게 동작한다면 status code를 확인해 어디서 문제가 발생했는지 파악해야 한다. 단순한 코드를 짤 때는(알고리즘 문제풀이 등) 주요 로직 위아래에 출력문을 찍어 결과를 확인했지만, 실제 프로그램을 개발할 때는 한 api가 동작하기까지 controller, service 등 여러 파일을 거치므로 어디부터 문제가 생긴 것인지 하나씩 출력하며 확인하기 어렵다. System.out.println을 사용하고 지울 때 꼭 필요한 코드를 지우게 될 수도 있다.

 

디버그 모드 어떻게 쓰는가?

1. 변수가 제대로 넘어왔는지, 원하는 동작이 이뤄졌는지 확인하고자 하는 행 번호를 더블클릭한다.

2. 디버그 모드로 어플리케이션을 실행한다.

3. API를 호출하면 그냥 실행했을때와 달리 브레이크 포인트를 찍어둔 부분을 한 줄씩 거치며 실행 과정을 확인할 수 있다.

4. 변수 상태값을 확인하거나 비정상 종료된 위치를 확인할 수 있다.

5. 코드를 수정하면 자동으로 부트가 재실행되고 새 인자를 넣어 테스트할 수 있다.

 

디버깅할 때 내가 명심해야 할 점

내 프로그램에는 다양한 오류가 있을 수 있다. 2주 정도 디버깅 늪에 빠져있으면서 느낀 점은 다음과 같다.

 

1. 문제가 실제로 문제가 맞는지 확인하고 난 후에 수정하자. 의심만으로 코드를 수정했다가 옳은 부분을 잘못되게 수정하고 되돌아온 적이 있다. 이런 고민은 실력을 늘게 해 주지도 않을뿐더러 시간만 낭비하게 한다!

2. 다양한 요인이 프로그램을 오동작하게 할 수 있다. 한번에 모두를 바꾸고 동작을 확인하지 말고 하나씩 확인하자.

 

 

Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.

프로젝트 import 하고 뜬 경고 문구!

롬복 쓰다가 @Data가 있는 곳마다 @EqualsAndHashCode로 callSuper 옵션 false를 주란다.

@Data는 constructor, getter, setter, toString, equals, hashcode 등 메서드를 자동으로 생성한다.

이때 만들어진 equals와 hashcode 메소드가 부모 클래스 필드까지 고려할지 안 할지를 설정할 수 있다.

- callSuper = true: 부모클래스 필드 값도 동일한지 체크

- callSuper = false: 본인클래스 필드 값만 고려

 

@Data는 아주 강력해서 코드를 간결하게 짤 수 있다는 장점이 있지만 각 메서드가 만들어졌을 때 코드에 어떤 영향을 주는지 확실하게 알지 못한 채 사용한다면 side effect가 생길 가능성이 다분하다......

 

이번 기회에 lombok에서 조심해서 써야하는 어노테이션을 몇 가지 알 수 있었는데,

평소에 종종 쓰던 @AllArgsConstructor, @RequiredArgsConstructor는 생성자를 편리하게 만들어준다.

이 어노테이션들은 클래스에 정의된 순서에 따라 생성자 파라미터 순서를 정하므로 특히 자료형이 같은 경우 예상치 않은 동작 결과를 낼 수 있다.

 

따라서 대안으로 생성자를 하나 만든 뒤 @Builder를 사용하는 것을 추천한다.

빌더 패턴에서는 파라미터 순서로 생성자를 만들었던 @AllArgsConstructor, @RequiredArgsConstructor와 달리, 이름으로 값을 설정하기 때문에 리팩터링 하기 용이하다.

 

관련해서 자세하게 소개해주시는 링크!

https://kwonnam.pe.kr/wiki/java/lombok/pitfall

 

java:lombok:pitfall [권남]

 

kwonnam.pe.kr

 

 

gradle은 왜 필요한가?

웹 애플리케이션을 짜면 컴파일 -> 빌드 -> 배포 과정을 거친다.

터미널에서는 컴파일을 수동으로 해야 하지만 이클립스 같은 IDE를 사용하면 run 동작시 class 파일로 변환 후 바로 실행해준다.

한 어플리케이션을 여럿이서 개발할 때 사용하는 플러그인 버전이나 의존성이 달라지면 빌드가 어렵다.

gradle은 계속해서 추가되는 라이브러리를 동기화해주는 빌드 자동화 도구이다.

 

gradle wrapper는 왜 필요한가?

당연한 말이지만 gradle을 쓰려면 gradle을 깔아야 한다.

각종 라이브러리 버전을 관리해주는 빌드 도구인 gradle도 버전을 탄다...

몇십 명에 달하는 개발자가 다른 gradle 버전을 쓴다면 오류 잡는데 한세월일 것이다.

gradle wrapper는 프로젝트에 종속된 gradle 버전을 사용할 수 있게끔 한다.

프로젝트에서 gradle 6.8을 쓰기로 했다면, 누구든 gradle wrapper를 이용해 이 프로젝트에 맞는 gradle 버전을 세팅할 수 있다. (로컬에 깔려 있는 java, gradle 버전은 무시해도 된다!)

10. Create a database on the cloud server

10.1. What is H2DB?

깃 정리를 할 때는 자세히 남기지 않았는데 section3에서 JPA를 공부할 때 h2라는 web console을 사용했습니다.
이번 섹션에서는 AWS에서 제공하는 RDS에 대해 알아볼텐데, 그 전에 이 데이터베이스와 RDS의 차이가 뭔지 알아보겠습니다!
데이터베이스는 영구 데이터베이스와 인 메모리 데이터베이스로 나누어 생각할 수 있습니다.

  • 영구 데이터베이스: 실제 메모리에 데이터를 유지하므로 서버가 반송되더라도 다시 사용할 수 있음
  • 인 메모리 데이터베이스: 데이터는 시스템 메모리에 저장되며 프로그램을 닫으면 데이터가 손실됨

H2DB는 자바 기반의 오픈소스 관계형 데이터베이스 관리 시스템입니다. 서버 모드와 임베디드 모드의 인메모리 DB 기능을 지원하고 브라우저 기반의 콘솔모드를 이용할 수 있으며 용량이 가볍습니다. 또한 표준 SQL 대부분 문법이 지원됩니다. 가볍고 빠르며 IntelliJ와의 호환성도 좋기 때문에 어플리케이션 개발 단계의 테스트 DB로써 많이 사용됩니다.
하지만 램에 데이터를 저장하다보니 웹서버를 재부팅하면 기존 데이터가 사라지고 저용량만 지원한다는 한계가 있습니다.

10.2. What is RDS?

만약 직접 데이터베이스를 설치하면 모니터링, 알람, 백업 등을 구성해야 하고 이는 시스템 관리를 복잡하게 만듭니다.
Amazon Web Service에서는 클라우드 기반의 Relational Database Service(RDS)라는 데이터베이스 관리 시스템을 제공합니다. RDS를 사용하면 개발자는 데이터베이스 설정, 패치 및 백업과 같은 운영 작업을 자동화해 개발 작업에만 집중할 수 있습니다. 만약 데이터베이스에 갑자기 많은 양의 데이터가 쌓여도 RDS를 사용하면 과금으로 손쉽게 용량을 늘릴 수 있습니다! 그러나 RDS는 과금을 해야 한다는 큰 단점이 있기 때문에 EC2 리눅스 위에 직접 DB를 설치하고 서비스하는 옵션도 생각해볼 수 있겠습니다.

RDS의 한계점

 

Quotas and constraints for Amazon RDS - Amazon Relational Database Service

By default, you can have up to a total of 40 DB instances. RDS DB instances, Aurora DB instances, Amazon Neptune instances, and Amazon DocumentDB instances apply to this quota. The following limitations apply to the Amazon RDS DB instances: 10 for each SQL

docs.aws.amazon.com

 

references

* 스프링 부트와 aws로 혼자 구현하는 웹 서비스 - 이동욱님 (👍)

* <https://developerhive.tistory.com/34>

* <https://dololak.tistory.com/285>

* <https://youngjinmo.github.io/2020/03/h2-database/>

* <https://ko.wikipedia.org/wiki/H2_(DBMS)>

9. Deploy to cloud server

9.1. Why do we need a cloud server?

앞 절에서 만든 Spring Boot 게시판 서비스를 모두가 사용하게 하려면 어떻게 해야 할까요?
외부에서 제가 만든 서비스에 접근하려면 24시간 작동하는 서버가 필요합니다.
이동욱 저자님은 24시간 서버에 세가지 선택지가 있다고 소개해주셨습니다.

  • 집에 24시간 PC 구동시키기
  • 호스팅 서비스 이용하기
  • 클라우드 서비스 이용하기

클라우드와 호스팅의 가장 큰 차이는 유연성입니다.
호스팅 서비스는 IDC에 물리 서버를 실제로 구축해서 서비스하지만
클라우드 서비스는 서버를 가상화해 사용자의 필요에 따라 실시간으로 확장과 축소가 가능하다는 점에서 이점이 있습니다.

9.2. Type of cloud server

Amazon에 따르면, Elastic Computer Cloud(EC2)는 안전하고 크기 조정이 가능한 컴퓨팅 용량을 클라우드에서 제공하는 웹서비스입니다. EC2는 개발자가 더 쉽게 클라우드 컴퓨팅 작업을 할 수 있도록 돕습니다.
예를 들어 서버 로그 관리, 모니터링, 하드웨어 교체, 네트워크 관리 등을 지원해줍니다. 제 게시판 앱은 IaaS에 해당하는 AWS EC2에 올라갔지만 사용자가 얼마나 서버를 관리하는지, 얼마나 기술을 제공받는지에 따라 클라우드 컴퓨팅은 몇가지 형태로 나뉩니다.

  • Infrastructure as a Service(IaaS)
    • 기업이 준비한 환경에서 개발자들이 장비를 선택할 수 있는 서버
    • 고객은 가상 서버 하위 레벨에 대해서 고려할 필요가 없다.
    • AWS의 EC2처럼 원하는 OS를 깔아 서버로 바로 사용할 수 있다.
  • Platform as a Service(PaaS)
    • 클라우드에서 컴파일해서 결과를 가져올 수 있게 하는 형태의 서버
    • 개발자는 node.js, java와 같은 런타임을 깔아놓고 소스코드만 적어서 빌드한다.
    • heroku, google app engine, ibm bluemix 등이 있다.
  • Software as a Service(SaaS)
    • 모든 것을 클라우드에서 제공하고 사용자는 별도 설치에 대한 부담이 없다.
    • public cloud에 있는 SW를 웹 브라우저로 불러와 언제 어디서든 사용할 수 있다.
    • 웹 메일, 구글 클라우드, 네이버 클라우드, 드롭박스 등이 있다.

references

* 스프링 부트와 aws로 혼자 구현하는 웹 서비스 - 이동욱님 (👍)

* <https://www.comworld.co.kr/news/articleView.html?idxno=49797>

* <https://wnsgml972.github.io/network/2018/08/14/network_cloud-computing/>

6. 로그인 기능 구현하기

 

구글/카카오/네이버 계정 사용해서 로그인하기 기능을 사용해보신 적이 있나요?

소셜 로그인은 우리가 처음 접하는 사이트에서 id와 password를 만들지 않고도 서비스를 사용할 수 있도록 돕습니다.

또한 개발자들을 api 구현의 고통에서 벗어나게 해주죠!

OAuth 로그인을 사용하면 개발자들은 아래와 같은 기능을 신경쓰지 않고 서비스 개발에만 집중할 수 있습니다.

 

* 로그인 시 보안

* 회원가입 시 이메일 혹은 전화번호 인증

* 비밀번호 찾기

* 비밀번호 변경

* 회원정보 변경

 

6.1. Spring Security

Spiring Sequrity는 스프링 기반 어플리케이션의 보안을 담당하는 프레임워크입니다.

보안과 관련된 다양한 옵션을 지원하고 java bean 설정만으로도 간단하게 사용할 수 있습니다.

본 섹션에서는 Spring Sequrity가 무엇이고 어떻게 동작하는지 보다도, OAuth에 사용된다는 점만 알고 넘어가려 합니다.

 

6.2. Google Login

구글 로그인을 제 SpringBoot 프로젝트와 연동시키기 위해 구글 클라우드 플랫폼에서 신규 서비스 정보를 만들었습니다. application-oauth.properties에 여기서 생성한 client id와 보안 코드를 붙여넣습니다.

spring.security.oauth2.client.registration.google.client-id="제 클라이언트 id"
spring.security.oauth2.client.registration.google.client-secret="제 클라이언트 secret"
spring.security.oauth2.client.registration.google.scope = profile, email

 

잊지말고 .gitignore에도 등록해주어야 합니다!

구글 로그인을 연동하기 위해 다음 과정을 거쳤습니다.

 

1. domain/user에 User 패키지를 생성합니다.

        @Getter
        @NoArgsConstructor
        @Entity
        
        public class User extends BaseTimeEntity {

            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;

            @Column(nullable = false)
            private String name;

            @Column(nullable = false)
            private String email;

            @Column
            private String picture;

            @Enumerated(EnumType.STRING)
            @Column(nullable = false)
            private Role role;
            @Builder
            public User(String name, String email, String picture, Role role) {
                this.name = name;
                this.email = email;
                this.picture = picture;
                this.role = role;
            }

            public User update(String name, String picture) {
                this.name = name;
                this.picture = picture;
                return this;
            }

            public String getRoleKey() {
                return this.role.getKey();
            }
        }

 

2. 각 사용자의 권한을 관리할 Enum 클래스 Role.java을 생성합니다.

        @Getter
        @RequiredArgsConstructor
        public enum Role {
            GUEST("ROLE_GUEST", "손님"),
            USER("ROLE_USER", "일반 사용자");
            private final String key;
            private final String title;
        }

 

3. USER의 CRUD를 다루기 위해 UserRepository.java를 생성합니다.

        public interface UserRepository extends JpaRepository<User, Long> {

        	Optional<User> findByEmail(String email);

        }

 

스프링 시큐리티를 설정하기 위해 다음 과정을 거쳤습니다.

 

1. build.gradle에 스프링 시큐리티 의존성을 추가합니다.

compile('org.springframework.boot:spring-boot-starter-oauth2-client')

 

2. config/auth 패키지를 생성해 관련 파일을 작성합니다.

    * config/auth/SecurityConfig.java

    * config/auth/CustomOAuth2UserService.java

    * config/auth/OAuthAttributes.java

    * config/auth/dto/SessionUser.java

 

references

* 스프링 부트와 aws로 혼자 구현하는 웹 서비스 - 이동욱님 (👍)

* <https://mangkyu.tistory.com/76>

* <https://sjh836.tistory.com/165>

+ Recent posts