http://victorydntmd.tistory.com/67
AWS 리소스에 대해 접근 권한을 부여하는 것이다.
개인에게 직접 부여할 수도 있고, 그룹에 권한을 부여한 뒤 사용자를 그룹에 포함시킬수도 있다.
유저를 생성하면 access key와 secret key가 저장된 csv 파일을 다운받을 수 있는데, 이는 외부에서 AWS 리소스 접근시에 사용되므로 잘 저장해두어야 한다.
더 많은 것을 기억하기 위해 기록합니다
http://victorydntmd.tistory.com/67
AWS 리소스에 대해 접근 권한을 부여하는 것이다.
개인에게 직접 부여할 수도 있고, 그룹에 권한을 부여한 뒤 사용자를 그룹에 포함시킬수도 있다.
유저를 생성하면 access key와 secret key가 저장된 csv 파일을 다운받을 수 있는데, 이는 외부에서 AWS 리소스 접근시에 사용되므로 잘 저장해두어야 한다.
서버에서 직접 multipart를 받아 S3 버킷에 업로드하면 서버쪽에서 multipart 파일을 쥐고 있어야 하는 둥 리소스 낭비가 크므로 S3에 접근할 수 있는 Presigned url을 생성하여 클라이언트가 직접 업로드하게 한다.
https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/dev/PresignedUrlUploadObjectJavaSDK.html
presigned url 이란 AWS S3 버킷에 바로 파일을 업로드 할 수 있는 URL을 말한다.
java sdk는 maven repository에서 간단하게 검색 가능하다
https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk
위 라이브러리를 임포트 하고 아래와 같이 작성하면 된다
1 | @Autowired // spring 사용 시 DI 가능하다! |
이렇게 생성된 presigned url에 PUT 요청으로 multipart data를 보내면 파일을 S3에 업로드할 수 있게 된다.
업로드는 expiration time 동안 유효하다.
참고로 위의 소스에는 접근하는 S3에 대한 인증정보가 없는데, 이는
AmazonS3
에서 사용하는ProfileCredentialProvider
때문이다.
이 클래스는 저장된 credential을 읽어서 인증하는데, 실행되는 환경에 따라 이 credential 을 찾는 우선순위가 있다.
(ec2에서 실행되면 ec2에 지정된 credential, pc에 저장된 credential 등등)
업로드 이후 presigned-url에서 query param을 제거하고 GET 요청하면 해당 파일을 보거나(이미지) 다운로드 받을 수 있는데, 그냥 접근하면 access denied가 발생한다.
그러므로 presigned url 생성할 때 public read
권한을 줘야하고, 아래와 같이 하면 된다.
http://zstd.github.io/amazon-presigned-url/
1 | GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectKey) |
docker로 3307 포트로 mysql을 띄웠는데 포트 변경해서 접속하는 옵션인 -P 를 아무리 줘도 무시되는 상황이 발생했다.
찾아보니 -h 가 localhost일 경우 소켓을 사용해서 포트 옵션이 무시되므로, 127.0.0.1을 써줘야 한다고 한다
https://serverfault.com/questions/306421/why-does-the-mysql-command-line-tool-ignore-the-port-parameter/306423?newreg=1c57059c005942bfbf0735b86bc570d6
1 | mysql -u root -h 127.0.0.1 -P 3307 |
스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트
전략을 기본으로 사용한다.
이 전략은 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고, 트랜잭션이 끝날 때 영속성 컨텍스트를 종료하는 방법이다.
스프링 트랜잭션 AOP는 @Transactional
어노테이션이 붙은 메서드가 호출될 때 트랜잭션을 시작한다.
메서드가 성공적으로 수행되면 해당 트랜잭션을 커밋하고, 예외가 발생한다면 트랜잭션을 롤백한다.
이 시점에 영속성 컨텍스트에 추가적인 작업을 호출한다.
1 | @Controller |
logic
메서드가 실행될 때 트랜잭션이 시작된다.logic
메서드가 종료되면 member, team에 대한 변경 내용이 데이터베이스에 플러시되고 트랜잭션이 커밋된다.트랜잭션과 영속성 컨텍스트의 생명주기가 같으므로, 트랜잭션이 끝남과 동시에 영속성 컨텍스트도 종료된다.
즉, HelloController에서 logic의 결과로 받은 Member 엔티티는 준영속 상태이다.
참고로 트랜잭션이 같으면 같은 영속성 컨텍스트를 사용한다.
위의 상황에서MemberRepository
와TeamRepository
는 서로 다른 엔티티 매니저를 주입받았지만 같은 영속성 컨텍스트를 사용한다.
이와 반대로 같은 엔티티 매니저를 사용해도 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다(잘 안그려진다 상황이…)
위에서 언급했듯이 트랜잭션과 영속성 컨텍스트의 생명주기가 같기 때문에, 트랜잭션이 끝난 뒤의 엔티티는 준영속 상태가 된다.
즉 위의 상황에서 logic
메서드가 끝남과 동시에 영속성 컨텍스트도 종료되었기 때문에 결과로 반환된 Member 엔티티는 준영속 상태가 되는 것이다.
그리고 당연하게도, 준영속 상태인 엔티티에 지연로딩을 수행하게 되면 오류가 발생한다.
1 | @Controller |
위처럼 지연로딩을 하게 되면 하이버네이트 기준으로 org.hibernate.LazyInitializationException
이 발생한다.
이는 영속성 컨텍스트에 들어있지 않은 준영속 상태의 엔티티에 지연로딩을 시도했기 때문에 발생하는 것이다.
트랜잭션이 끝나면서 영속성 컨텍스트도 같이 종료되었기 때문에 반환된 엔티티는 자연스럽게 준영속 상태가 되었고, 이런 현상이 발생한 것이다.
하지만 생각해보면, 영속성 컨텍스트가 트랜잭션과 동시에 종료되고 프레젠테이션 계층까지 전파되지 않는것은 좋은 선택(?)이다.
만약 영속성 컨텍스트를 프레젠테이션 계층까지 열어두었다면 프레젠테이션 계층에서도 변경 감지가 동작하게 되어 위험하고,
각 계층이 가지는 역할자체도 모호해지기 때문이다.
하지만 위에서 봤다시피, 지연로딩이 동작하지 않는다는 점은 꽤나 골치아픈 일이다.
결국 위의 상황을 해결하고 싶으면 아래의 2가지 방법을 사용해야 한다.
말 그대로 영속성 컨텍스트가 살아있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해서 반환하는 방법이다.
아래의 3가지 방법이 있다.
fetchType을 EAGER로 바꾸는 방법이다.
1 | class Member{ |
Member 조회 시 항상 Team을 같이 로딩해서 가지게 되므로, 준영속 상태가 되어도 지연로딩 문제가 발생하지 않는다.
이미 로딩해서 가지고 있기 때문이다.
하지만 이 방식은 아래와 같은 문제를 가진다.
사용하지 않는 엔티티를 로딩한다
뷰에서 Team이 필요하지 않은 경우도 있을것이다. 하지만 항상 Team을 같이 조회해야 한다.
N+1 문제가 발생한다
1 | String sql = "SELECT m FROM Member m"; |
실행되는 SQL은 아래와 같다.
1 | SELECT * FROM Member; |
JPQL을 실행할 때는 글로벌 페치 전략을 참고하지 않고 오직 JPQL만 참고하여 충실히 SQL을 만들기 때문에 발생한 현상이다.
위와 같이 처음 조회한 수만큼 다시 SQL을 사용해서 조회하는 것을 N+1 문제라고 한다.
봤다시피 fetchType을 EAGER로 바꾸는건 너무 비효율적이다.
fetchType을 LAZY로 설정하고 필요할 때만 같이 조회해오도록 하는것이 좋을 것이고, JPQL에서는 FetchJoin이라는 기능으로 이를 지원한다.
사용법은 간단하다.
1 | String sql = "SELECT m FROM Member m JOIN FETCH m.team"; |
간단히 조인 명령어 마지막에 FETCH
만 넣어주면 된다.
이렇게 하면 해당 대상까지 조인으로 함꼐 조회해온 뒤 엔티티에 바인딩해준다.
이 방식이 현실적인 대안이긴 하지만, 화면에 맞춘 리파지토리 메서드가 증가할 수 있다는 단점이 있다.
즉, 아래와 같은 메서드들이 생길 수 있다는 것이다.
repository.findMember
메서드repository.findMemberWithTeam
메서드이런식으로 계속 메서드가 추가되다보면 레파지토리와 뷰 간의 논리적인 의존관계가 발생하게 된다.
이런 상황에서는 최적화를 조금 포기하고 논리적 의존관계를 최소화하는 방법을 선택하던지(findMember와 findmemberWithTeam 통합),
최적화를 선택하고 논리적 의존관계를 가지고 가던지… 선택해야 한다.
사실상 성능에 미치는 영향이 미비하므로 뷰와 레파지토리의 의존관계가 급격하게 증가하는 것보다는 최적화를 포기하는것이 조금 나은 것 같다.
영속성 컨텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해서 반환하는 방법이다.
1 | Member member = em.find(Member.class, 1); |
강제로 초기화했으므로 준영속 상태에서도 사용할 수 있게된다.
하이버네이트를 사용한다면 initialize
메서드를 사용해요 프록시를 강제로 초기화할 수 있다.
1 | org.hibernate.Hibernate.initialize(member.getTeam()); |
하지만 이것도 결국 생각해보면 프리젠테이션 계층이 은근슬쩍 서비스 계층을 침범하는 상황이다.
프리젠테이션 계층에 필요한 엔티티를 서비스 계층에서 초기화하고 있기 때문이다.
프리젠테이션 계층과 서비스 계층 사이에 FACADE 계층이라는 것을 둬서 이런 논리적 의존관계를 완전히 제거할 수 있다.
FACADE 계층이랄게 특별할 건 없고, 그냥 중간에 계층하나를 더 두고
서비스 계층에서 받은 엔티티를 프리젠테이션 계층에서 필요한 형태로 가공해서 내려다주는 역할을 하는 것이다.
지연로딩 때문에 영속성 컨텍스트가 필요하므로, 트랜잭션은 FACADE 계층부터 시작해야한다.
얼핏보면 이렇게 함으로써 논리적 의존관계가 완전히 제거된 듯 보이지만,
실용적인 관점에서 보면 결국 코드를 훨씬 많이 작성하게 되고, 단순히 서비스 계층 호출을 위임하는 코드가 생길 가능성이 많다.
OSIV(Open Session In View)는 영속성 컨텍스트를 뷰 까지 열어준다는 뜻이다.
영속성 컨텍스트가 살아있으면 엔티티는 영속 상태가 유지되므로, 뷰에서도 지연로딩을 사용할 수 있다.
이 기능의 핵심은, 뷰에서도 지연 로딩이 가능하다
이다.
뷰까지 영속성 컨텍스트를 열어두기 위해 가장 간단한 방법을 사용한다.
요청이 들어오자마자 필터나 인터셉터에서 트랜잭션을 시작하고, 요청이 끝날때 트랜잭션도 끝내는 것이다(트랜잭션과 영속성 컨텍스트의 생명주기가 같기 때문)
이렇게 하면 뷰에서도 지연로딩이 가능하므로 엔티티를 미리 초기화할 필요가 없다. 당연히 FACADE 계층도 필요없어진다.
하지만 트랜잭션이 프레젠테이션 영역까지 열리므로 프레젠테이션 계층이 엔티티를 변경할 수 있는
심각한 문제가 생긴다.
1 | @Controller |
뷰를 렌터링 한 후 트랜잭션이 커밋될 것이고, 이때 변경 내역이 플러시되면서 회원의 이름이 XXXX로 변경되는 참사가 발생할 것이다.
프레젠테이션 계층은 데이터를 보여주는 계층이다. 이런 행위가 절대 허용되어서는 안된다.
이를 막기 위한 방법들은 아래와 같다.
엔티티를 읽기 전용 인터페이스로 제공
1 | interface MemberView{ |
엔티티 래핑
1 | class MemberWrapper{ |
DTO
전통적인 방법으로, 단순히 데이터만 전달하는 객체인 DTO를 만들고 보여줄 엔티티의 값을 세팅해서 내려주는 방법이다.
사실 이 방법은 OSIV의 장점을 못 살리는 방법이다. 강제로 초기화의 조금 다른 방법일 뿐이다.
이러한 문제점들로 인해 요청 당 트랜잭션은 거의 사용하지 않는다.
요즘에는 서비스 레벨까지만 트랜잭션을 내리는 방법을 사용하는데, 이게 스프링 프레임워크에서 제공하는 OSIV가 선택한 방식이다.
아래는 spring-orm.jar
에 들어있는 클래스들 중 하나이다. 필요한 위치에 따라 선택해서 사용하면 된다.
org.springframework.org.jpa.support.OpenEntityManagerInViewFilter
org.springframework.org.jpa.support.OpenEntityManagerInViewInterceptor
스프링 OSIV는 아래와 같이 OSIV를 사용하기는 하지만 트랜잭션은 비즈니스 계층에서만 사용한다.
참고로 여기서 강제로 플러시를 호출해도 예외(
javax.persistence.TransactionRequiredException
이 발생한다)
다시 간단하게 정리하면,
이런것이 가능한 이유는, 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 되기 때문이다. 이를 Nontransaction reads
라고 한다.
(트랜잭션 없이 엔티티를 변경하면 javax.persistence.TransactionRequireException
이 발생한다)
상당히 많은 부분이 해결되었지만, 여기에도 여전히 주의사항이 존재한다.
바로 프레젠테이션 계층에서 엔티티를 수정한 직후에 트랜잭션을 시작하는 서비스 계층을 만났을 경우
이다.
1 | @Controller |
결과는 member의 이름이 XXXX로 바껴버린다.
기존의 트랜잭션 단위로 영속성 컨텍스트를 열던 것을 요청 단위로 영속성 컨텍스트를 여는 것으로 확장
했기 때문에
하나의 요청에서 여러 트랜잭션이 있을 경우 영속성 컨텍스트를 공유하게 되고, 위와 같은 현상이 발생하는 것이다.
나는 OSIV를 이용해 엔티티를 프레젠테이션까지 내리는 행위는 딱히 좋지 않다고 생각한다.
위와 같은 문제점도 있고, 엔티티와 프레젠테이션 사이에 논리적인 의존관계가 생기는 것을 막을 수 없게 될 테니까…
전통적인 방법인 필요한 애들만 DTO로 만들어서 내려주는 방법이 가장 괜찮은 것 같긴한데,
이것또한 서비스 영역과 프레젠테이션 영역에 은근한 의존관계가 생긴다는 말을 반박할수는 없는 것 같다.
영속성 컨텍스트가 리퀘스트까지 열린다는 것은, 커넥션을 유지하는 시간이 늘어남을 의미한다
이는 트래픽이 많아지면 OSIV를 사용하면 영속성 컨텍스트를 유지하는 영역이 트랜잭션 이상으로(request까지) 넓어지고,
이는 커넥션을 맺는 시간이 길어짐을 의미한다
(게다가 하나의 요청내에서는 db작업 외에 다른 많은 작업들이 수행될 수 있으므로(네트워크 통신 등), 커넥션을 계속 열어두는 행위는 좋지않다)
request 까지 커넥션을 유지하게 됨. 데이터베이스에서 데이터를 가져와야 하기 때문이다
이는 성능상 이슈를 발생시킬 수 있으므로(커넥션 풀 개수 부족), OSIV를 끄고 service에서 DTO로 변환해서 내려주는 방식을 택하는 것이 좋다
위에서 반박할 수 없다고 해놓고…
JPQL은 기본적으로 1 -> N 방향으로 조인이 1번 까지밖에 안된다
2번 이상하게 되면 MultipleBagException
이 발생한다.
1 | // item아래 재고단위, 색상 등이 1:N으로 있다고 가정 |
카테시안 곱이 일어나기 때문이라는데…(너무 많은 로우가 생기므로)
@OneToMany 관계를 List가 아닌 Set으로 바꿔주면 위 에러가 발생하지 않는다
https://www.thoughts-on-java.org/hibernate-tips-how-to-avoid-hibernates-multiplebagfetchexception/
이유는 모르겠다 젠장…
https://github.com/Rebilly/ReDoc
작성한 OAS를 보기좋은 document로 만들어준다.
https://github.com/Rebilly/ReDoc/blob/master/cli/README.md
1 | redoc-cli bundle <spec file> |
하면 생성된다
https://github.com/OpenAPITools/openapi-generator
Open Api Spec을 통해 code를 generate 해주는 라이브러리
server stub, client stub 생성 가능
openapi generator를 gradle에서 사용하기 위한 플러그인
1 | dependencies { |
1 | apply plugin: 'org.openapi.generator' |
allOf를 사용하면 generate된 코드에 상속을 사용할 수 있다
1 | ChildDTO: |
아래와 같이 generate 됨
1 | class ChildDTO extends ParentDTO{ |
git : https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md
swagger : https://swagger.io/docs/specification/basic-structure/
1 | openapi: 3.0.0 |
https://swagger.io/docs/specification/data-models/data-types/
paths
아래에 정의한다.
1 | paths: |
https://swagger.io/docs/specification/describing-parameters/
기본 형태는 아래와 같음
1 | paths: |
in
에는 path, query, header, cookie
가 올 수 있음https://swagger.io/docs/specification/describing-request-body/
1 | paths: |
https://swagger.io/docs/specification/components/
components
아래에 정의하고, 재사용을 목적으로 한다.
$ref
속성을 이용하고 #
으로 참조한다.
1 | $ref: '#/components/schemas/Item' |
#
는 현 위치를 말한다.
일반 오브젝트(DTO 등)
https://swagger.io/docs/specification/describing-parameters/#common-for-various-paths
Common Parameters for Various Paths
부분 참조
https://swagger.io/docs/specification/describing-responses/
status 별로 선언가능하며 description, object 등을 내려줄 수 있다
1 | paths: |
https://www.intricity.com/data-warehousing/master-data-vs-transaction-data/
마스터 데이터 : 사람이나 기관이 관리하는 데이터
트랜잭션 데이터 : 마스터 데이터에 대한 이벤트 데이터 같은 것. 구매, 주문 등
Spring orm이 제공하는 것 LocalContainerEntityManagerFactory
테이블의 기본키 이름은 테이블이름을 prefix로 가져가는것이 좋다
엔티티의 경우 참조라는 값이 있지만 RDB의 경우는 없기 때문
게다가 테이블간 관계설정할 때 외래키에 테이블의 이름을 쓰는 관례가 많으므로 헷갈리지 않기 위해 기본키도 테이블 이름을 prefix로 가져주는 것이 좋다
@PersistenceContext :
스프링이나 J2EE를 사용하면 컨테이너가 직접 엔티티매니저를 관리하고 제공해준다.
이 어노테이션은 컨테이너가 관리하는 엔티티 매니저를 주입하는 어노테이션이다
@PersistenceUnit : EntityManagerFactory를 주입받고자 할 때
@Transactional은 RuntimeException만 롤백한다
public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}
public void addOrder(Order order){
orders.add(order);
order.setmember(this);
}
근데 책에서 설명할때는 bug를 방지하여 조건검사를 어느정도 넣어주는것을 봤는데, 이걸 제거해도 되는것인지?
회원 중복검사를 하는 로직은 service 메서드에서 private으로 구현했다
이미 등록된 회원일 경우 exception을 반환하는 식으로 처리했다
이러한 검증 로직이 있어도 멀티스레드 안정성을 보장하기 위해 UK를 걸어주는 것이 좋다
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException(“need more stock”);
}
this.stockQuantity = restStock;
}
public void cancel() {
if (delivery.getStatus() == DeliveryStatus.COMP) {
throw new RuntimeException("이미 배송완료된 상품은 취소가 불가능합니다.");
}
this.setStatus(OrderStatus.CANCEL);
for (OrderItem orderItem : orderItems) {
orderItem.cancel();
}
}
관계를 한방에 맺어주는 편의메서드를 제공한다. 이로인해 객체간의 관계도 파악가능하다.
public static Order createOrder(Member member, Delivery delivery, OrderItem… orderItems) {
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for (OrderItem orderItem : orderItems) {
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(new Date());
return order;
}
추가적인 정보를 제공해주는 메서드를 만든다
public int getTotalPrice() {
int totalPrice = 0;
for (OrderItem orderItem : orderItems) {
totalPrice += orderItem.getTotalPrice();
}
return totalPrice;
}
다른 엔티티와 값을 동기화한다(재고 등)
public static OrderItem createOrderItem(Item item, int orderPrice, int count) { // 주문생성시 재고를 깎고
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
public void cancel(){ // 주문취소시 재고를 더한다
getItem().addStock(count);
}
—
도메인 모델 패턴과 트랜잭션 스크립트 패턴
도메인 모델 패턴은 대부분의 비즈니스 로직이 엔티티에 속해있고, 서비스는 역할을 위임하고 트랜잭션 바인딩 정도로 사용되것이다
트랜잭션 스크립트 패턴은 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 말한다
수정에 대해서는 ‘영속화 + 필요한 메서드 변경’ 을 사용하던 ‘병합’을 사용하던 상관없다고 말하고 있음