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

 게임에서 보면 캐릭터 종족별로 공격력이나 방어력이 제각각이다. 

 

이런 캐릭터 클래스를 절차 지향부터 어떻게 전략 패턴을 활용하는지 살펴보자

 

게임상 캐릭터 종족에따라 방어력이 달라지는 코드를 예를 들어보면 다음처럼 if문을 통하면 될 거 같다.

 

if 문으로 방어력 구분

public class Character {
    private int attack;
    private int defense;
    private String type;

    public int defense() {
        if(type.equals("오크")) {
            this.defense = 100;
        } else if(type.equals("휴먼")) {
            this.defense = 50;
        } else if(type.equals("트롤")) {
            this.defense = 40;
        }
        return defense;
    }
}

 위 코드의 문제는 종족이 추가 될 때마다 if문 코드를 수정 삭제해야 한다는 문제가 있다. 그리고 코드가 절차 지향적이다. 문제가 있어 보여 캐릭터별 클래스를 만들면 어떨까 해서 적용해 봤다. 

 

개별적 클래스로 만들어 직접 의존하기

public class Ork {
    private int attack;
    private int defense;
    
    public int defense() {
        this.defense = 100;
        return defense;
    }
}

public class Human {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 50;
        return defense;
    }
}

public class Trol {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 40;
        return defense;
    }
}

이에 따른 Human 클래스는 다음과 같다.

public int defense() {
        if(type.equals("오크")) {
            Ork ork = new Ork();
            this.defense = ork.defense();
        } else if(type.equals("휴먼")) {
            Human human = new Human();
            this.defense = human.defense();
        } else if(type.equals("트롤")) {
            Trol trol = new Trol();
            this.defense = trol.defense();
        }
        return defense;
    }

 뭔가 코드가 바뀐거 같지만, 여전히 if문이 난무하고 의존을 클라이언트(Character)에서 직접 하고 있다. 여전히 종족이 추가되면 Character 클래스의 수정이 필수적이다. 

 

 아.. 어떻게 하지... 상속이란것도 배웠는데...!!

 

상속을 썻지만 직접 의존

 다형성의 객체지향 개념으로 상속이란 개념을 적용해 보는 코드를 살펴보자. 

// 종족 추상클래스
public abstract class Tribe {
    private int attack;
    private int defense;

    abstract int defense();
}

public class Ork extends Tribe {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 100;
        return defense;
    }
}

public class Human extends Tribe {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 50;
        return defense;
    }
}

public class Trol extends Tribe {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 40;
        return defense;
    }
}

Character 클래스는 다음과 같이 변경된다. 

public int defense() {
        Tribe tribe = null;
        if(type.equals("오크")) {
            tribe = new Ork();
        } else if(type.equals("휴먼")) {
            tribe = new Human();
        } else if(type.equals("트롤")) {
            tribe = new Trol();
        }
        this.defense = tribe.defense();
        
        return defense;
    }

 뭔가 다형성까지 적용해 객체지향스러움을 자랑하고 싶지만 여전히 if문은 제거하지 못했다.... 이제 Stratey 패턴이 등장할 때가 왔다. 

 

 스트래태지 패턴의 원칙인 바뀌는 부분을 찾아 바뀌지 않는 것으로부터 분리시켜 캡슐화하는 것부터 해보자. 여기서 캡슐화란 객체가 해야 할 기능 중 일부를 별도의 그룹으로 뽑아서 캡슐화한다는 뜻이다. 내가 사용하는  스프링 IoC에서 스트레테지 패턴을 편하게 해 준다.

 변하는 부분은 Character 클래스에서 변하는 부분은 종족에 따른 방어력 부분이다. 이를 캡슐화시켜보자. 방어력 부분을 인터페이스로 만들고 이를 구현한 구체 클래스를 만들고 Character 클래스에서 이를 집약시키면 된다. 

public interface CharacterDefense {
    int defense();
}

public class Ork implements CharacterDefense {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 100;
        return defense;
    }
}

public class Human implements CharacterDefense {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 50;
        return defense;
    }
}

public class Trol implements CharacterDefense {
    private int attack;
    private int defense;

    public int defense() {
        this.defense = 40;
        return defense;
    }
}

 위 코드에 따른 Character 코드는 다음과 같다. 

public class Character {
    private int attack;
    private CharacterDefense characterDefense;

    public void setCharacterDefense(CharacterDefense characterDefense) {
        this.characterDefense = characterDefense;
    }

    public int defense() {
        return characterDefense.defense();
    }
}

 와우... 수많은 if문이 사라졌고, Character는 CharaterDefense의 구체적인 부분을 몰라도 된다. 즉 의존성도 확 줄었다. 종족이 추가된다면 Character 클래스는 바뀔 필요가 없어졌다. OCP도 만족한다. 

 

전략 패턴의 의도

 동일 계열의 알고리즘군(CharacterDefense)을 정의하고, 각 알고리즘을 캡슐화하여 이를 상호작용할 수 있게 만든다. 

 

활용성

 다음 상황에서 전략 패턴을 활용할 수 있다. 

  • 행동들이 조금씩 다를 뿐, 개념적으로 관련된 많은 클래스들이 존재할 때. 전략 패턴은 많은 행동 중 하나를 가진 클래스를 구성할 수 있는 방법을 제공한다.
  • 알고리즘의 변형이 필요할 때
  • 사용자가 몰라야 하는 데이터를 사용하는 알고리즘이 있을 때.
  • 하나의 클래스가 수많은 행동을 정의하고, 이런 행동들이 클래스 안에서 수많은 분기분들로 이뤄졌을 때.

 

 전략 패턴은 실전에서도 많이 사용된다. 나 같은 경우에도 전 회사에서 상품 주제별 상품 리스트를 보여주는 서비스가 있었는데, 전략 패턴으로 많은 이득을 보았다. 그럼 이것으로 마무리를 지으려 한다.

 

위 코드는 하단에서 확인할 수 있다. 

https://github.com/jinioh88/design_pattern/tree/master/src/main/java/com/study/design_pattern/strategy 

  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

 최근 스프링 부트는 테스트 유닛으로 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에 대한 이해와 흐름을 파악하며 개발할 수 있을 것 같다.

+ Recent posts