출처: https://blog.iroot.kr/341 [RootKR] 출처: https://blog.iroot.kr/341 [RootKR]

이번 시간에는 막걸리 리뷰를 하고자 한다. 

 

편의점에서 쇼핑을 하다 백종원 프로에서 나왔다는 골목 막걸리가 있어 집어 보았다. 

 

 

같은 편의점이라도 지점마다 안 파는 것 같으니 집 주변을 유심히 살펴보자.

 

그럼 바로 리뷰로 들어 가겠다.

 

잔에 따랐을 때 색은 완전 불투명한 색이다. 아침햇살을 연상케 하는 투명도다. 

 

일단 향에서는 요구르트의 유산균의 냄새와 밤의 단맛이 함께 올라온다. 

 

마셔보니 탄산은 거의 없는 편이고, 알콜의 기운도 별로 느껴지지 않는다. 

 

요구르트의 산뜻함이 느껴지고 바디감은 막걸리가 입안 전체와 혀를 감싸는 듯한 느낌이다. (끈적임 아님...)

 

막걸리 특유의 누룩의 맛은 느껴지는 바가 없이 단조로운 맛이다. 

 

한줄평 : 요구르트에 알콜탓나??

 

재구매 의사 : 없다.

 

 

  OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어둔다는 뜻이다. 이렇게 되면 뷰에서도 지연 로딩을 사용할 수 있게 된다. 

 

과거 OSIV: 요청 당 트랜잭션

 요청 당 트랜잭션 방식의 OSIV는 클라이언트의 요청이 들어오자마자 서블릿 필터나 스프링 인터셉터에서 트랜잭션을 시작하고 요청이 끝날 때 트랜잭션도 끝내는 방식이다. 

 

요청 당 트랜잭션 방식의 OSIV 문제점

 문제는 컨트롤러나 뷰 같은 프리젠테이션 계층이 엔티티를 변경할 수 있다는 점이다. 

@RestController
@RequiredArgsConstructor
public class MemberController {
   private final MemberService memberService;
  
   public String viewMemberName(Long id) {
       Member member = memberService.findOne(id);
       member.setName("XXX");
      
       return member.getName();
   }
}

 위 코드의 의도는 고객 이름을 그냥 "XXX"로 뷰에만 노출하려는 의도였는데, 변경 감지로 인해 데이터베이스도 함께 변경돼버린 참사가 일어났다. (실제로 실무에서 이런 식으로 하는 작업했던 동료 덕에 많은 것을 배웠다...^^). 서비스 계층이 아닌 프리젠테이션 계층에서 데이터 변경은 유지보수하기 힘들어진다. 프리젠테이션에서 엔티티를 수정하지 못하게 막는 방법은 다음과 같다. 

  • 엔티티를 읽기 전용 인터페이스로 제공
  • 엔티티 레핑
  • DTO만 반환

 요즘은 위 문제들로 인해 트랜잭션 범위를 프리젠테이션 계층까지 열어두지 않는다. 스프링 프레임워크가 제공하는 OSIV는 위 문제를 보완해 비즈니스 계층에서만 트랜잭션을 유지하는 방식의 OSIV를 사용한다. 

(현업에선 Spring Boot를 사용하고 있는데, 스프링 부트는 OISV를 OpenEntityManagerInViewInterceptor로 기본으로 하고 있기 때문에 프리젠테이션까지 트랜잭션이 살아 있는 듯 하다.)

스프링이나 J2EE 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 그리고 트랜잭션은 보통 서비스 계층에서 시작하므로 서비스 계층이 끝나는 시점에 트랜잭션이 종료되면서 영속성 컨텍스트도 함께 종료된다. 

 

 따라서 조회한 엔티티가 서비스와 레포지토리가 아닌 컨트롤러나 프리젠테이션 계층에서는 준영속 상태가 된다. 즉 트랜잭션이 없는 프리젠테이션 계층에서는 병경 감지와 지연 로딩이 동작하지 않는다. 

@RestController
@RequiredArgsConstructor
public class OrderController {
   private final OrderRepository orderRepository;
  
   public String view(Long orderId) {
       Order order = orderRepository.findOne(orderId);

       // 준 영속상태의 member. 아직 lazy 발동안됨
       Member member = order.getMember();
       member.getName(); // 사용시 Lazy 발동. 예외 발생
      
       return member.toString();
   }
}

 

준영속 상태와 변경 감지

 변경 감지 기능은 영속성 컨텍스트가 살아 있는 서비스 계층까지만 동작하고 영속성 컨텍스트가 종료된 프리젠테이션 계층에서는 동작하지 않는다. 보통 변경 감지 기능은 서비스 계층에서 비즈니스 로직을 수행하면서 발생한다. (근데 Spring Boot OSIV에선 컨트롤러까지 영속성 트랜잭션이 살아있다...)

 

 비즈니스 로직은 서비스 계층에서 끝내고 프리젠테이션 계층은 데이터를 보여주는 데 집중해야 한다. 변경 감지 기능이 프리젠테이션 계층에서 동작하면 유지보수가 어려워질 수 있다. 

 

준영속 상태와 지연로딩

 준영속 상태에서 지연 로딩이 동작하지 않아 지연로딩 객체 조회 시 프록시 객체가 사용된다. 프록시는 실제 데이터가 아니가 때문에 프록시 객체를 사용하면 실제 데이터를 불러오려고 초기화를 시도한다. 준영속 상태에선 지연로딩에 문제가 발생하므로, LazyInitialaztionException 예외가 발생한다. 

 

 준영속 상태의 지연로딩 문제를 해결하는 방법은 크게 2가지다. 

  • 뷰가 필요한 엔티티를 미리 로딩해 두는 방법
  • OSIV를 사용해 엔티티를 항상 영속 상태로 유지하는 방법

 뷰가 필요한 엔티티를 미리 로딩하는 방법을 살펴보자. 이 방법은 영속성 컨텍스트가 살아 있을 때 뷰에 필요한 엔티티들을 미리 다 로딩하거나 초기화해 반환하는 방법이다. 따라서 엔티티가 준영속 상태로 변해도 연관된 엔티티를 이미 다 로딩해 두어 지연 로딩이 발생하지 않는다. 

 

 뷰가 필요한 엔티티를 미리 로딩해 두는 방법은 어디서 미리 로딩하느냐에 따라 3가지 방법이 있다. 

 

1. 글로벌 페치 전략 수정

 가장 간단한 방법은 지연 로딩에서 즉시 로딩으로 변경하는 것이다. 

@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "member_id")
private Member member;

 fetch 전략을 EAGER로 하면 다음과 같이 프리젠테이션 계층에서 사용할 수 있다. 

public String view(Long orderId) {
   Order order = orderRepository.findOne(orderId);
  
   Member member = order.getMember();
   member.getName(); // 이미 다 로딩되서 오류 안남

   return member.toString();
}

 위의 방법은 편리하기는 하나 다음 단점이 있다. 

  • 사용하지 않는 엔티티를 로딩한다. 
    • 위 코드에서 만약 나는 Order만 조회하고 싶었는데, 쓸데없는 Member도 조회하는 꼴이 된다. 
  • N+1 문제가 발생한다. 
    • em.find()를 실행(엔티티 매니저)하면 EAGER로 설정 시 SQL은 Order와 Member를 left outer join 해서 한꺼번에 가져온다. 
    • 문제는 jpql에서 발생하는데, JPA가 jpql을 분석해 SQL을 생성할 때는 글로벌 페치 전략을 참고하지 않고 오직 jpql 자체만 사용한다. 
    • 만약 조회한 Order 엔티티가 10개라면 select * from Order 쿼리가 한번 날아가고, Order마다 해당하는 Member를 조회하려 10번의 select * from Member where id = ? 쿼리가 날아가는 문제가 생긴다.
    • 위 문제는 jpql 패치 조인으로 해결할 수 있다. 

2. JPQL 페치 조인

 페치 조인을 사용하면 SQL Join을 사용해 페치 조인 대상까지 함께 조회한다. 따라서 N+1 문제가 해결된다. 하지만 이 역시 단점이 존재한다. 

  • 무분별하게 사용하면 화면에 맞춘 리포지토리에 메서드가 증가할 수 있다. 즉 Order만 필요한 화면이 있고 Order와 Member 둘 다 필요한 화면이 있다고 할 때 api를 Order 전용과 Order를 Member와 페치 조인한 api가 2개나 생겨 버린다. 
  • 실무에서 나도 이 페치 조인 때문에 api 함수 개수가 늘어나 관리가 힘들었다...ㅠㅠ

 

3. 강제로 초기화

 강제로 초기화는 영속서 켄텍스트가 살아있을 때 프리젠테이션 계층이 필요한 엔티티를 강제로 초기화해 반환하는 방법이다. 

@Transactional(readOnly = true)
public Order findOrder(Long id) {
   Order order = orderRepository.findOne(id);
   order.getMember().getName();  // getMember()후 그 속성을 읽으면 초기화가 된다. 
  
   return order;
}

 

 글로벌 페치 전략으로 즉시 로딩하면 연관된 엔티티가 실제 엔티티가 아닌 프록시 객체로 조회된다. 프록시 객체는 사용하는 시점(getName())에 초기화된다. 위 예제처럼 프리젠 테이션 계층에 필요한 프록시 객체를 영속성 컨텍스트가 살아있을 때 강제로 초기화해 반환하면 이미 초기화했으므로 준영속 상태에서도 사용할 수 있게 된다. 

 위의 문제는 서비스 계층이 프리젠테이션 계층을 위한 프록시 초기화 역할을 하게 되어 계층을 침범하는 문제가 생기는데, 이는 FACADE 계층이 담당하면 된다. 

 

FACADE 계층 추가

 서비스 계층과 프리젠테이션 계층 사이에 FACADE 계층을 하나 더 두는 방법이다. 뷰를 위한 프록시 초기화는 이곳에서 담당하면 된다. 프록시를 초기화하려면 영속성 컨텍스트가 필요하므로 FACADE에서 트랜잭션을 시작해야 한다. 

 

FACADE 계층의 역할과 특징

  • 프리젠테이션 계층과 도메인 모델 계층 간의 논리적 의존성을 분리
  • 프리젠테이션 계층에서 필요한 프록시 객체를 초기화함
  • 서비스 계층을 호출해 비즈니스 로직을 실행
  • 리포지토리를 직접 호출해 뷰가 요구하는 엔티티를 찾음
@RequiredArgsConstructor
public class OrderFacade {
   private final OrderService orderService;
  
   public Order findOrder(Long id) {
       Order order = orderService.findOrder(id);
       order.getMember().getName(); // 프록시 초기화
      
       return order;
   }
}

 

 서비스 계층과 프리젠테이션 계층 간 논리적 의존관계를 제거했지만, 실용적 관점에서 볼 때 FACADE의 단점은 중간에 계층이 하나 더 끼어들었다는 점이다. 

 

'개발 이야기 > JPA' 카테고리의 다른 글

[개발이야기 - JPA] OSIV를 알아보자  (0) 2020.05.12
[개발이야기 - JPA] 병합 - merge()  (0) 2020.05.02

오늘 리뷰할 맥주는 성산 일출봉이라는 에일 계열의 맥주다

 

편의점에서 4캔 만원 해서 패키지로 구입하였다. 

 

생긴건 상큼상큼해 보인다.

 

상품명 : 성산 일출봉 에일

스타일 : 블론드 에일

알콜도수 : 5.1%

생산국 : 대한민국

 

 

블론드 에일이라고 해서 순도 100% 황금빛 일 줄 알았으나, 탁산 금색에 가깝다. 겉만 봐선 바이젠이다.

 

맥주잔에 따랏을 때 거품 유지력이 좋다.

 

첫 향에서 비스켓의 고소함이 올라오며 특별하게 뚜렷한 향은 느껴지지 않는다.

 

향을 느끼기 위해 집중을 해보니 풀 계열의 홉향이 올라온다.

 

마셔보니 혓바닥으로 전해지는 탄산이 오밀조밀하며 미세한 오렌지와 시트러스함이 입안에 전해진다. 

 

미세하게 동서 벌꿀의 향도 느껴 진다. 

 

끝 맛은 홉의 쓴맛이 맴돈다. 

 

재구매 의사 : 없다. 

 최근 스프링 부트는 테스트 유닛으로 JUnit4에서 JUnit5(jupiter)로 변경되었다. 토비의 스프링을 스프링 부트 버전으로 연습하다가 기존에 사용했던 JUnit4 방식의 Exception 테스트가 안돼 찾아보고 정리를 해 보았다. 

 

User를 가져오는 get() 메서드

   public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = dataSource.getConnection();

        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        User user = null;
        if(rs.next()) {
            user = new User();
            user.setId(rs.getString("id"));
            user.setName(rs.getString("name"));
            user.setPassword(rs.getString("password"));
        }

        rs.close();
        ps.close();
        c.close();

        if(user == null)
            throw new EmptyResultDataAccessException(1);

        return user;
    }

 위 메서드에서 user를 가져오지 못한다면 EmptyResultDataAccessException을 던지도록 하고 이것을 테스트 해보고 싶었다. 

 

JUnit4에서의 방식

@Test(expected = "EmptyResultDataAccessException.clss)
    public void getUserFailure() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDAO dao = context.getBean("userDAO", UserDAO.class);
        dao.deleteAll();
        assertThat(dao.getCount()).isEqualTo(0);

        dao.get("unkown_id");
    }

 위처럼 @Test에 expected로 해당 예외를 담아주면 알아서 자동으로 예외 테스트를 할 수 있었다. 

 

그래서 JUnit5도 같겟지 하면서 @Test에 expected를 주니 빨간줄이 난무하기 시작한다. JUnit5에선 기존 방법으로 테스트를 못하고 다음과 같이 테스트를 하면 된다. 

@Test
    public void getUserFailure() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

        UserDAO dao = context.getBean("userDAO", UserDAO.class);
        dao.deleteAll();
        assertThat(dao.getCount()).isEqualTo(0);

        assertThrows(EmptyResultDataAccessException.class, () -> {
            dao.get("unkown_id");
        });
    }

 assertThrows에 필요한 클래스를 등록하고 람다식으로 예외를 던질 실행문을 작성하면 된다. 

arguments 객체

자바스크립트는 함수를 호출할 때 함수 형식에 맞춰 인자를 넘기지 않더라도 에러가 발생하지 않는다. 

function func(arg1, arg2) {
    console.log(arg1, arg2);
}
 
func();  // undefined undefined
func(1); // 1 undefined
func(1, 2); // 1 2
func(1, 2, 3); // 1 2

 위 예제에서 func(), func(1) 호출처럼 함수의 인자보다 적게 함수를 호출할 경우, 넘겨지지 않은 인자에는 undefined값이 할당된다. 이와 반대로 인자가 초과된 경우 초과된 인수는 무시된다. 

 

 자바스크립트의 이런 특성 때문에 함수 코드를 작성할 때, 런타임 시 호출된 인자의 개수를 확인하고 이데 따라 동작을 다르게 해줘야 하는 경우가 생긴다. 이를 가능하게 하는 게 arguments라는 객체다. 

 

 자바스크립트에서는 함수를 호출할 때 인수들과 함께 암묵적으로 arguments 객체가 함수 내부로 전달 된다. arguments 객체는 함수를 호출할 때 넘긴 인자들의 배열 형태로 저장된 객체를 의미한다. 여기서 주의할 점은 진짜 배열이 아니고 배열과 유사한 동작하는 객체이다. 

 

 arguments 객체는 매개변수 개수가 정확하게 정해지지 않은 함수를 구현하거나, 전달된 인자의 개수에 따라 서로 다른 처리를 해줘야 하는 함수를 개발하는데 유용하게 사용될 수 있다.

function sum() {
    var result = 0;
 
    for(var i = 0; i < arguments.length; i++) {
        result += arguments[i];
    }
 
    return result;
}
console.log(sum(1,2,3));
console.log(sum(1, 2, 3, 4, 5, 6, 7, 8));

 

호출 패턴과 this 바인딩

 자바스크립트에서 함수를 호출할 때 기존 매개변수로 전달되는 인자값에 더해, arguments 객체 및 this 인자가 함수 내부로 암묵적으로 전달된다. 여기서 this는 자바스크립트의 여러 가지 함수가 호출되는 방식에 따라 this가 다른 객체를 참조하기 때문에 중요한 개념이다. 

 

객체의 메서드를 호출할 때 this 바인딩

 객체의 프로퍼티가 함수일 경우, 이 함수를 '메서드'라 부른다. 이러한 메서드를 호출할 때, 메서드 내부 코드에서 사용된 this는 해당 메서드를 호출한 객체로 바인딩된다. 

var myObject = {
    name: 'foo',
    sayName: function () {
        console.log(this.name);
    }
};
 
var otherObject = {
    name: 'bar'
}
 
otherObject.sayName = myObject.sayName;
 
myObject.sayName();  // foo
otherObject.sayName();  // bar

 위 코드에서 sayName() 메서드에서 사용된 this는 자신을 호출한 객체에 바인딩된다. 따라서 myObject.sayName()의 this는 myObject 객체를 가리킨다. 

 

함수를 호출할 때 this 바인딩

 자바스크립트에서 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩 된다. 즉 브라우저에서 자바스크립트를 실행하면 전역 객체는 window객체가 된다. 자바스크립트의 모든 전역 변수는 실제로 이러한 전역 객체의 프로퍼티들이다. 

var foo = "I'm foo";
 
console.log(foo);  
console.log(window.foo);

 위 에서의 전역변수 foo는 전역 객체(window)의 프로퍼티로 된다. 즉 window.foo로 접근 가능해진다.

 

var test = 'This is test';
console.log(window.test);
 
var sayFoo = function() {
    console.log(this.test);  // sayFoo() 호출시 this는 전역 객체에 바인딩 됨.
}
 
sayFoo();

 위 코드에서 전역 변수 test를 선언하고 전역 객체 window의 프로퍼티로 접근했다. sayFoo()가 호출되면 this는 전역 객체에 바인딩되므로, window에 바인딩된다. 그래서 this.test는 window.test를 의미하게 된다. 

 

 여기서 주의할 점이 있다. 내부 함수에서 this를 사용할 때 주의를 해야 한다. 

var myObject = {
    value: 100, 
    func1: function() {
        this.value += 1; // 메서드에서 this는 호출하는 것을 가리키므로 myOject를 가리킴
        console.log('func1() called ' + this.value);
 
        // 내부함수
        func2 = function() {
            this.value += 1;  // myObject를 가리킬까???
            console.log('func2() called ' + this.value);
 
            // 내부함수
            func3 = function() {
                this.value += 1; // this가 myObject를 가리킬까??
                console.log('func3() called ' + this.value);
            }
            
            func3(); // 호출
        }
 
        func2(); // 호출
    }
};
 
myObject.func1(); 

 위 코드에서 앞에서 설명했던 것처럼 this는 메서드가 호출한 것을 가리키니 func1, func2, func3의 this가 모두 myObject를 가리킬 것만 같은 느낌이다. 그래서 사람의 두뇌는 다음을 예상한다. 

func1() called 2
func2() called 3
func3() called 4

 

 하지만 실제론 다음의 결과가 나온다.

func1() called 2
func2() called 101
func3() called 102

 왜 그런고 하니 자바스크립트에선 내부 함수 호출 패턴을 정의해 놓지 않았기 때문이다. 내부 함수는 메서드가 아닌 함수이므로, 이를 호출할 때 함수로 취급된다. 따라서 내부 함수의 this는 전역 객체(window)에 바인딩된다. 

 

 이렇게 내부 함수가 this를 참조하는 한계를 극복하려면 부호 함수의 this를 내부 함수가 접근 가능한 다른 변수에 저장하는 방법이 사용된다. 보통 관례상 that이라는 이름으로 지정한다. 

// 전역 변수 value
var value = 100;
 
var myObject = {
    value: 1, 
    func1: function() {
        var that = this;  // func1()의 this값을 that에 저장. 
 
        this.value += 1; // 메서드에서 this는 호출하는 것을 가리키므로 myOject를 가리킴
        console.log('func1() called ' + this.value);
 
        func2 = function() {
            that.value += 1;  
            console.log('func2() called ' + that.value);
 
            func3 = function() {
                that.value += 1; 
                console.log('func3() called ' + that.value);
            }
            
            func3(); // 호출
        }
 
        func2(); // 호출
    }
};
 
myObject.func1(); 

 

 이제 결과는 처음 예상한 데로 다음과 같다.

func1() called 2
func2() called 3
func3() called 4

 

생성자 함수를 호출할 때 this 바인딩

자바스크립트에선 기존 함수에 new 연산자를 붙여 호출하면 해당 함수는 생성자 함수로 동작한다. 여기서 문제는 일반 함수에 new를 붙여 호출하면 의도치 않는 생성자 함수처럼 동작하게 된다는 것이다. 그래서 자바스크립트 스타일 가이드에서는 생성자 함수를 일반 함수와 분류하기 위해 생성자 함수의 첫 문자를 대문자로 쓰라고 권하고 있다. 

// Person() 생성자 함수
var Person = function(name) {
    this.name = name;
};
 
var foo = new Person('foo');
console.log(foo.name);  // foo

 생성된 Person 객체는 생성자 함수 코드에서 사용되는 this로 바인딩된다. 

 

객체 리터럴 방식과 생성자 함수를 통한 객체 생성 방식의 차이

 가장 큰 차이는 객체 리터럴 방식으로 생성된 객체는 같은 형태의 객체를 재생성할 수 없다는 점이다. 

객체 리터럴의 경우 자신의 프로토타입 객체는 Object가 되고, 생성자 함수의 경우는 실제 생성한 객체(위 코드에서 Person)로 서로 다르다. 

 

// 객체 리터럴 방식으로 foo 생성
var foo = {
    name: 'foo',
    age: 35,
    ggender: 'man'
};
console.dir(foo);
 
// 생성자 함수
function Person(name, age, gender, position) {
    this.name = name;
    this.age = age;
    this.gender = gender;
}
 
var bar = new Person('bar', 33, 'man');
console.dir(bar);
 
var baz = new Person('baz', 25, 'woman');
console.dir(baz);

 

 이런 차이가 발생하는 이유는 자바스크립트 객체 생성 규칙 때문이다. 자바스크립트 객체는 자신을 생성한 생성자 함수 prototype 프로퍼티가 가리키는 객체를 자신의 프로토타입 객체로 설정한다. 즉 리터럴 방식에선 객체 생성자 함수는 Object()이고, 생성자 함수 방식의 경우 생성자 함수 자체(Person)이므로 두 가지 방식이 다른 프로토타입 객체를 가리킨다. 

 

생성자 함수를 new를 붙이지 않고 호출할 겨우

 일반 함수 호출과 생성자 함수를 호출할 때 this 바인딩 방식이 다르다. 일반 함수 호출의 경우 this가 전역 객체(window)에 바인딩되고, 생성자 함수 호출의 경우 this는 새로 생성된 빈 객체에 바인딩된다. 

// new 없이 호출
var qux = Pserson('qux', 20, 'man');
console.log(qux);  // undefined
 
console.log(window.name); // qux
console.log(window.age); // 20

 위 코드에서 new 없이 일반 함수를 호출한 경우, this는 함수 호출이 되므로 전역 객체인 window 객체로 바인딩된다. 따라서 this.name이 window.name이 되어버린다. 위 코드에서 전역 객체로 name, age, gender가 선언되진 않았지만, Person이 호출되면서 window 객체에 동적으로 name, age, gender 프로퍼티가 생성된다. 

 

call과 apply 메서드를 이용한 명시적인 this 바인딩

 apply()나 call() 메서드는 this를 원하는 값으로 명시적으로 매핑해 특정 함수나 메서드를 호출할 수 있다. 

// 생성자 함수
function Person(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
}
 
var foo = {};  // 빈객체 생성
 
// apply() 메서드
Person.apply(foo, ['foo', 30, 'man']); // 두번째 인자에 배열로 넘김.
console.dir(foo);
 
// call() 메서드
Person.call(foo, 'foo', 30, 'man');
console.dir(foo);

 

함수 리턴

자바스크립트 함수는 항상 리턴값을 반환한다. 

 

규칙 1) 일반 함수나 메서드는 리턴값을 지정하지 않은 경우, undifined 값이 리턴된다. 

var noReturnFunc = function() {
    console.log('Hello');
};
 
var result = noReturnFunc();
console.log(result); // Hello  undefined 찍힘

 

규칙 2) 생성자 함수에서 리턴값을 지정하지 않을 경우 생성된 객체가 리턴된다. 

 위 규칙으로 생성자 함수에선 일반적으로 리턴값을 지정하지 않는다. 만약 생성자 함수의 리턴값으로 불린, 숫자, 문자열이 온다면 이 리턴값을 무시하고 this로 바인딩된 객체를 리턴한다. 

function Person(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
 
    return 100;  // 객체가 아닌 숫자를 리턴하고 있음
}
 
var foo = new Person('foo', 30, 'man');
console.log(foo); // 100이 아닌 Person 객체 찍힘

 병합은 준영속 상태의 엔티티를 다시 영속 상태로 변경할 때 사용한다. merge() 메서드는 준영속 상태의 엔티티를 받아 그 정보로 새로운 영속 상태의 엔티티를 반환한다. 

 

@RequiredArgsConstructor
public class ExMain {
   private final EntityManager em;


   @Transactional
   public void update() {
       // 준영속 상태의 객체
       Member member = new Member("오세진", 32);
       member.setAge(30);
   }
}

 위 코드에서 member는 준영속 상태의 객체다. 아직 JPA에 관리된 상태가 아닌 순수한 객체이기 때문이다. 여기서 아무리 setXXX 메서드로 값을 변경해 보았자, 변경감지에 걸리지 않아 디비에 반영이 되지 않는다. 

 

 이를 변경하고 싶다면 준영속 상태의 엔티티를 영속 상태로 변경해야 하는데, 이때 merge()를 사용하면 된다. 

@RequiredArgsConstructor
public class ExMain {
   private final EntityManager em;

   @Transactional
   public  void update() {
       // 준영속 상태의 객체
       Member member = new Member("오세진", 32);
       member.setAge(30);

       // 영속 상태의 객체
       Member mergedMember = em.merge(member);
       mergedMember.setAge(32); // 변경감지
   }
}

 위 코드에서 영속상태의 객체를 변경감지 동작을 통해 변경된 데이터가 Transaction이 끝나면서 데이터베이스에 반영된다. 여기서 주의할 점은 파라미터로 넘어온 member 객체는 여전히 준영속 상태라는 점이다. 

 

merge()의 동작 방식

  1. merge()를 실행
  2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회
    • 만약 1차 캐시에 엔티티가 없으면 데이터베이스에 엔티티를 조회하고 1차 캐시에 저장.
    • 무조건 1번은 db 조회를 하므로 성능에 좋지 않을 수 있다. 
  3. 조회한 영속 엔티티에 member 엔티티의 값을 채워 넣음
    • 이때 member의 모든 값을 영속 엔티티에 채워 넣기 때문에 null 값이 들어갈 수 도 있는 문제가 생긴다.
    • 이래서 업데이트 시 merge()보단 변경 감지를 사용하자.
  4. 영속 상태의 객체를 반환

 

 병합은 파라미터로 넘어온 엔티티의 식별자 값으로 영속성 컨텍스트를 조회하고, 찾는 엔티티가 없으면 데이터베이스에서 조회한다. 만약 데이터베이스에도 없다면 새로운 엔티티를 생성해 병합한다. 따라서 병합은 save or update 기능을 수행한다고 볼 수 있다.

 

현업에서 jpa의 동작을 재대로 이해하지 못한 체 데이터를 update 치려다 수많은 삽질을 했던 경험이 있다. 이 기능들을 잘 이해하고 나니 update에 대한 이해와 흐름을 파악하며 개발할 수 있을 것 같다.

작년까지 강화권으로만 조행을 가다, 믿고 가는 포인트들이 다들 공사 중이라 4월 초에 충남으로 조행을 떠났다.

 

가기 전에 봄 배스에 관한 많은 유튜브를 보면서, 스피너베이트와 미노우를 적극 활용해 마릿수를 해야겠다는 생각으로 아는 형과 함께 떠났다. 

 

첫 포인트에서 아는 형님이 4짜를 낚는 것을 보고 역시 믿음의 충남을 가슴속으로 외치며 낚시를 해보았지만...

 

1시간 이상을 지져도 배스는 나올 생각을 안 한다.

 

요런 새물과 석축이 있는 아름다운 포인트였지만, 피딩 타임이 지나서였을까 반응이 없다. 

 

12시쯤 다른 포인트로 이동!!

 

다른 포인트에 도착하자마자 수몰 나무가 보인다. 

 

수몰나무가 보이길래 스피너 베이트를 던져본다.

 

발 앞에서 루어를 회수하는데 배스가 나타나더니 스피너 베이트를 물었다.

 

흥분해서 낚싯대를 드는데, 털림과 동시에 허무함과 스피너 베이트의 믿음이 겹친다.

 

그늘진 곳을 찾다 다리 밑에 수초가 보인다.

 

어제 본 유튜브에서 스피너 베이트로 수초 주변을 긁으면 배스가 나온다고 한 게 기억이 났다.

 

스피너베이트 걸리면 어떡하지의 걱정이 1초 떠올랐으나, 배스를 한 마리도 못 잡고 서울로 복귀한다는 끔찍한 생각에 일단 던져 본다.

 

던지고 슬슬 감는데 액션이 이상하다. 속도를 좀 더 내어 감아본다. 

 

갑자기 수초 사이를 지나는데 묵직함이 느껴진다. 

 

설마 배스인가??? 후킹을 해본다. 묵직한 것이 딸려 나온다. 배스다~!!

 

역시 유튜브 채널 하시는 분들의 가르침은 위대하다.

 

한수를 하고 똑같은 자리에 또 스피너베이트로 긁어 본다. 

 

또 묵직함과 함께 한 마리가 더 나온다. 역시 배스는 무리 지어 다니나 보다.

 

기쁨과 함께 이곳저곳을 더 공략해 보았지만 배스는 더 이상 나타나지 않아 2시쯤 철수하기로 한다.

 

이번 조행기는 프리리그만 고집하던 나에게 하드베이트에 대한 좋은 인상을 준 그런 출조였다.

+ Recent posts