728x90

코드는 모두 깃허브에 있음.

일단 자바의 컬렉션 인터페이스들의 특징부터 나열한다.

컬렉션

  • Collection
    • 자바가 제공하는 최상위 컬렉현, Hibernate는 중복을 허용하고, 순서를 보장하지 않는다고 가정
  • Set
    • 중복을 허용하지 않고, 순서도 보장하지 않는다.
  • List
    • 순서가 있는 컬렉션아며 중복을 허용한다.
  • Map
    • Key, Value 구조로 되어있는 컬렉션이다.

JPA와 Collection

Hibernate는 엔티티를 영속 상태로 만들 때 컬렉션 필드를 Hibernate

준비한 컬렉션으로 감싸서 사용한다.

다음 예시를 보자

@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;

    @OneToMany
    private Collection<Member> members = new ArrayList<>();
}

@DataJpaTest
class TeamTest {

    @PersistenceUnit
    EntityManagerFactory emf;

    EntityManager em;

    EntityTransaction tx;

    @BeforeEach
    void setUp() {
        em = emf.createEntityManager();
        tx = em.getTransaction();
        tx.begin();
    }


    @Test
    void 컬렉션_테스트() {
        Team team = new Team();
        System.out.println(team.getMembers().getClass());
        em.persist(team);
        System.out.println(team.getMembers().getClass());
    }
}

테스트코드 지만 단순히 이 결과를 확인하기 위해서 콘솔 출력을 진행하였다.

결과는 이렇게 나온다.

image
{: text-center}

처음 객체를 포장할때는 Team 엔티티 클래스에서 명시한 ArrayList로 포장을 하는데 엔티티를 영속상태로 바꿔주는 순간 PersistentBag 으로 변경된다.

Hibernate는 컬렉션을 효율적으로 사용하려고 영속상태로 만들때 원본의 컬렉션을
감싼 내장 컬렉션을 생성하여 이 감싼 내장 컬렉션을 사용하도록 참조를 변경한다.

그렇기 때문에 컬렉션을 사용하려면 즉시 초기화를 해주고 사용하는걸 권장한다.

다음은 Hibernate의 내장 컬렉션들과 특징이다.

컬렉션 내장컬렉션 중복 순서
Collection, List PersistentBag O X
Set PersistentSet X X
List + @OrderColumn PersistentList O O

Collection, List

CollectionList는 엔티티를 추가할 때 중복된 엔티티가 있는지 비교하지 않고

왜❓ - 중복을 허용하기 때문

단순히 저장만 하면 된다. 그렇기 때문에 엔티티를 추가해도 지연 로딩된 컬렉션을 초기화 하지 않는다.

Set

Set은 엔티티를 추가할 때 중복값을 확인하기 때문에 서로 비교를 해야한다.

그렇기 때문에 지연 로딩된 컬렉션을 초기화 한다.

List + @OrderColumn

@OrderColumn은 DB에 순서값을 저장해서 조회할 때 사용한다는 의미

순서가 있기에 DB에 순서값도 관리하는데

단점이 있어 사용하지 않는다고 한다.

순서값을 DB가 가지고 있기 때문에 하나를 지운다고 가정하면 삭제된 List의 번호에는 null이 저장된다.

NullPointerException우려

@OrderBy

책에서 나온것처럼 특정 칼럼에 @OrderBy를 주는 법도 있겠지만 이렇게 하지않고 대부분 Auditing 기능 오버라이드 하여 한다고 한다.

@Converter

컨버터는 단어 그대로 형 변환을 해주는 것이다.

예를들어 boolean 타입은 DB에 저장될 때 0과 1로 저장이 된다. 대신에 Y나 N으로 저장하고 싶다면

컨버터를 사용하면 된다.

@Converter
public class BooleanYNConverter implements AttributeConverter<Boolean, String> {

    @Override
    public String convertToDatabaseColumn(Boolean attribute) {
        return (attribute != null && attribute) ? "Y" : "N";
    }

    @Override
    public Boolean convertToEntityAttribute(String dbData) {
        return "Y".equals(dbData);
    }
}

이렇게 AttributeConverter를 구현해주고 @Converter를 명시해준다.

//방법 1
@Convert(converter = BooleanYNConverter.class, attributeName = "적용할 변수")
public class Test {
    //방법 2
    @Convert(converter = BooleanYNConverter.class)
    private boolean 변수명;
}

이렇게 있다. 그리고 추가로 모든 boolean에 대해서 적용을 시켜준다면

클래스최상단에 @Converter(autoApply = true)를 주면 된다.

리스너

JPA 리스너 기능을 사용하면 엔티티의 생명주기에 따른 이벤트 처리 가능

image
{: text-center}

이벤트의 종류와 발생 시점은 위의 이미지와 같다.

  1. PostLoad : 엔티티가 영속성 컨텍스트에 조회된 후 또는 refresh 호출한 후(2차 캐시에 저장되어 있어도 호출).
  2. PrePersist : persist() 를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에. 식별자 생성전략을 사용한 (이하 @GeneratedValue) 경우 엔티티에 식별자는 아직 존재하지 않는다. 또한 새로운 인스턴스를 merge할 때.
  3. PreUpdate : flushcommit을 호출해서 엔티티를 DB에 수정하기 직전
  4. PreRemove : remove()를 호출해 엔티티를 영속성 컨텍스트에서 삭제하기 직전. 영속성 전이가 일어날 때, orphanRemoval(고아객체 관련)에 대해선 flushcommit시에
  5. PostPersist : flushcommit을 호출해서 엔티티를 DB에 저장한 직후 호출. 식별자 항상 존재함. 생성전략이 IDENTITY면 식별자를 생성하기 위해 persist()를 호출한 직후 바로 호출.
  6. PostUpdate : flushcommit을 호출해서 엔티티를 DB에 수정한 직후
  7. PostRemove : flushcommit을 호출 엔티티를 DB에 삭제한 직후

이벤트 적용위치

적용 위치는 3가지이다.

  • 엔티티에 직접 적용
  • 별도의 리스너 등록
  • 기본 리스너 사용

엔티티에 직접 적용

@Entity
public class Entity {
    @Id @GeneratedValue
    private Long id;

    ...

    //아래로 쭉 구현
    @PrePersist
    public void prePersist() {
        ...
    }

    @PostPersist
    public void postPersist() {
        ...
    }
    ...

}

별도의 리스너 등록

이거는 JPA Auditing 생각해보면 될거같다. 결국 AuditingEntityListener 이 리스너도 안에 어노테이션으로 아래와 같이 구현되어있다.

@Configurable
public class AuditingEntityListener {

    private @Nullable ObjectFactory<AuditingHandler> handler;

    public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {

        Assert.notNull(auditingHandler, "AuditingHandler must not be null!");
        this.handler = auditingHandler;
    }

    @PrePersist
    public void touchForCreate(Object target) {

        Assert.notNull(target, "Entity must not be null!");

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markCreated(target);
            }
        }
    }

    @PreUpdate
    public void touchForUpdate(Object target) {

        Assert.notNull(target, "Entity must not be null!");

        if (handler != null) {

            AuditingHandler object = handler.getObject();
            if (object != null) {
                object.markModified(target);
            }
        }
    }
}

여러개의 리스너를 등록했을 때 호출순서는

  1. 기본 리스너
  2. 부모 클래스 리스너
  3. 리스너
  4. 엔티티

와 같다.

엔티티 그래프

엔티티 그래프는 엔티티를 조회하는 시점에 연관된 엔티티들을 함께 조회하는 기능이다.
Named 엔티티 그래프는 Named쿼리 자체의 빈도수가 낮기때문에 다루지 않겠다.

EntityGraph<Team> graph = em.createEntityGraph(Team.class);

graph.addAttributeNodes("속성");

JPAQuery<Emp> query = queryFactory.selectFrom(Q클래스).where(조건);

query = query.setHint("javax.persistence.fetchgraph", graph);

query.fetchOne();

이렇게 엔티티 그래프를 정의하고 Hint로 그래프를 넣어주면 되는 방식이다.

정리

엔티티 그래프는 항상 조회하는 엔티티의 ROOT경로에서 시작해야 한다.

만약 Member엔티티에 Team이 포함되어 있다면 Member조회 후 Team으로 가야되는데 역으로 갈 수는 없다.

영속성 컨텍스트에 엔티티가 이미 로딩되어 있다면 엔티티 그래프 적용 ❌

fetchgraph와 loadgraph의 차이는 loadgraph는 엔티티 그래프의 설정한 속성과 함께 글로벌 페치전략이 FetchType.EAGER 인 관계들도 전부 포함해서 함께 조회한다.

728x90

'JPA' 카테고리의 다른 글

JPA template 이슈  (0) 2022.08.07
고급 주제와 성능 최적화 1  (0) 2022.08.06
준영속 상태의 지연로딩을 해결하는 방법  (0) 2022.08.06
JPA metamodel must not be empty!  (0) 2022.08.06
728x90

📌 SOLID 원칙

객체 지향적으로 설계하는데에 있어 기본이 되는 SOLID원칙에 대해서 알아보자.
SOLID는 각 원칙의 첫번째 글자를 따서 만든 것이다.

  • 단일 책임 원칙 (Single responsibility principle; SRP)
  • 개방-폐쇄 원칙 (Open-closed principle; OCP)
  • 리스코프 치환 원칙 (Liskov substitution principle; LSP)
  • 인터페이스 분리 원칙 (Interface segregation principle; ISP)
  • 의존 역전 원칙 (Dependency inversion principle; DIP)

이 원칙들이 서로 다른 내용이라 하기에는 밀접하게 연관되어 있으므로 한꺼번에 같이 이해해야 할 것이다.

📌 단일 책임 원칙

객체 지향은 책임을 객체에게 할당하는데에 있다. 객체로 존재하게 하는 이유가 책임인데

이 원칙은 원칙 이름에서도 알 수 있듯 책임과 관련되어 있다.

📢 클래스는 단 한 개의 책임을 가져야 한다.

하나의 클래스가 여러 책임을 갖게되면 책임마다의 이유가 발생하기 때문에 클래스 변경이 잦아질 수 있다.

그래서 한개의 이유로만 클래스가 변경되려면 하나의 책임만을 가져야 한다.

지금 진행하고있는 TDD강좌를 들으면서 SOLID원칙 그리고 디자인 패턴에 대해 많이 학습하는것 같다.

1-2-3 과정으로 진행되는 과정이 예를 들어 있다고 한다면

public class Process {
    public void one() {
        /// 1...
        String test = two();
        three(test);
    }


    public void two() {
        /// 2...
    }

    public void three(String test) {
        /// 3...
    }
}

이렇게 작업을 하게되면, 연쇄적으로 코드 수정을 해야할 수 밖에 없다.

이것은 코드를 분리만했지 절차지향과 다를게 없다.

여기서 만약 String타입의 인자가 아니라 다른타입으로 오게되면 twothree 모두 바꿔야한다.

극단적인 예시이지만, 같은 작업을 수행하는 것이라면 괜찮지만
데이터를 불러오고, 가공하고, 보여준다 라면 얘기는 다르다.

그러면 클래스마다 1개씩 잘라서 분리를 해주어야 한다.

그래서 최종적으로 동작하는 클래스에서 조립하는 느낌으로 구현을 한다고 생각하면 이해가 쉬울 듯 하다.

📌 개방-폐쇄 원칙

개방, 폐쇄 말만들으면 열고 닫는것.

확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다.

말이 이렇게만 보면 어렵다.

기능을 변경하거나 확장은 할수 있되, 그 기능을 사용하는 코드는 수정을 하지 않아야 된다.

이부분은 추상화와 관련이 깊은것 같다. 사용하는 코드를 수정하지 않아야한다 라는 원칙이기 때문에

인터페이스 또는 추상클래스를 사용했을 시에 구현체에 따라 다르게 동작할 수 있게 만듬으로써

변경 확장이 용이하며, 코드 수정은 하지 않게된다.

개방 폐쇄 원칙이 깨질때의 증상 1 - 다운 캐스팅

이것은 다운 캐스팅을 할 경우에 그런데, instanceof 로 타입 다운을 시켜 검증을 하면

발생하는 것인데

내 경우에는 볼링게임 여기서 사용했었다.

객체마다 다르게 동작할 수 있는 무언가가 있다면 그 메소드를 추상화하여 동작하게 만들어야한다.

개방 폐쇄 원칙이 깨질때의 증상 2 - if-else

public void draw() {
    if (index == 1) {
        // 1조건
    }else if (index == 2) {
        // 2조건
    }
    ...
}

위 코드처럼 조건이 하나씩 늘게되면 else if로 늘려서 추가할 경우이다.

draw에서 그냥 메소드를 통일하고

Pattern이라는 인터페이스를 생성하고 방식에따라 다른 로직을 구현하고 메서드는 통일시키는 식으로

추가를 해주자.

개방 폐쇄 원칙은 유연함에 대한 것

이 원칙은 변화가 예상되는 것들을 추상화해서 유연함을 얻도록 해줬다.

그러니까 추상화를 하지 않거나 아직 개념이 부족해 못한다면

개방-폐쇄 원칙을 지킬 수 없게 되어 기능 변경이나 확장을 어렵게 만든다는 것이다.

내가 퇴사했던 회사에서 딱 이렇게 개발을 했었다. 오히려 이게 쉽다면서... 난 아니었는데 😥

각설하고, 코드의 변화 요구가 발생하면 이 구현부를 추상화해서 지금의 이 원칙에 맞게 수정할 수 있는지

항상 생각하면서 개발해야겠다.

📌 리스코프 치환 원칙

윗부분의 개방폐쇄에선 추상화 그리고 다형성을 이용하여 구현되었다.

이번에 알아보는 이 리스코프 치환 원칙은 개방 폐쇄를 조력해주는(?) 다형성에 관한 원칙이다.

상위 타입의 객체를 하위 타입의 객체로 치환하여도

상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

이거는 예제를 따로 개발자가 반드시 정복해야할 객체지향과 디자인 패턴에서 인용하였다.

부모 클래스와 자식 클래스가 있다고 가정. 특정한 메서드는 상위 타입인 부모클래스를 이용할 것이다.

public void someMethod(SuperClass sc) {
    sc.someMethod();
}

someMethod는 부모타입의 객체를 사용하고 있는데, 이 메소드에 하위타입 객체를 전달해도 someMethod

정상적으로 동작해야 한다. 이것이 바로 리스코프 치환 원칙이다.

중요❗❗❗ 이 원칙이 제대로 지켜지지 않는다면 개방-폐쇄역시 지켜지지 않는다.

개념적으로 상속 관계에 있는 두개의 클래스가 있더라도 막상 구현했을때는 상속 관계가 아닐 수도 있는 것들이 있다.

이럴 때는 상속이 아니라 각자 다른 타입으로 매칭을 시켜줘야한다.

📌 인터페이스 분리 원칙

인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

이 원칙또한 앞에서와 연관이 있는데 결국 용도에 맞게 인터페이스를 분리하는 것은

단일 책임 원칙과 연결이 된다. 하나의 타입에 여러 기능이 섞일 경우

한가지의 기능 변화로 인해 다른 기능이 영향을 받을 가능성이 높아진다.

그래서 인터페이스를 분리한다면 한 기능에 대한 변경의 리스크를 줄일 수 있게 된다.

그리고 인터페이스의 분리가 잘되어 있다면 즉, 단일 책임 원칙이 잘 지켜질때

구현클래스들의 재 사용성이 높아질 수 있기 때문에

결국 이 원칙은 인터페이스와 구현 클래스의 재사용성을 높인다는 효과를 가지게 된다.

원칙은 클라이언트에 대한 것

인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하란 원칙이다.

클라이언트가 사용하는 기능들을 중심으로 인터페이스를 분리하여 클라이언트에서 발생하는 인터페이스

변경의 여파가 다른 클라이언트에 미치는 것을 최소화 할 수가 있다.

📌 의존 역전 원칙

고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.

저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.

고수준 모듈은 저수준 모듈을 조합해서 한 모듈을 만들어 놓은 형태이고

저수준 모듈은 도메인단위로 분리했을 때 상세한 행동 하나하나를 정의한 것을 말한다.

고수준 모듈이 저수준 모듈에 의존할 떄의 문제

내가 생각하는 이 SOLID 원칙. 어떤 법칙을 읽던간에 무조건 인터페이스가 생각나는게 당연한걸까❓

이것도 내가 생각하기엔 추상화가 답인것 같다.

구현하는 비즈니스 클래스가 여러개가 있다고 한다면 개방폐쇄의 예시처럼 조건일때 다른 클래스 로직을 불러와

실행하는 것이 될것이다.

이런 방식이 프로그램을 변경하는데 너무 어렵게 만든다. 이 저수준 모듈이 변경됨에 따라

고수준은 변경이 되지 않게끔 하려면 나오는 원칙이 의존 역전 원칙이다.

그러니까 이 의존 역전 원칙이 리스코프나 개방-폐쇄를 따르는 설계를 만들어주는 밑바탕이 되는 것으로 볼 수 있다.

의존 역전

마땅한 코드 예시가 없어서 책에서 한번더 가져다 쓴다.

public class FlowController {
    public void process() {
        //소스 코드에서 FileDataReader에 대한 의존 발생
        FileDataReader reader = new FileDataReader();
    }
}

public class FileDataReader implements ByteSource {
    ...
}

이런 구조가 있다면

소스코드의 의존은 아래와 같다.

image
{: text-center}

그런데 런타임에서 본다면
FlowController -> FileDataReader 가 되는것이다.

왜냐면 컨트롤러가 시작점이고 거기서 하나씩 로드해서 참조하기 때문에

이런 구조가 나온다.

왜?

이유는 그냥 단순하다. 절차지향으로 생각해보면 저 Controller를 동작시켜야 아래가 동작하지 않는가?

정리

지금까지 SOLID 원칙에 대해 알아보았다. 결국 이 원칙이 말하는 바는

정말 원초적으로 생각하고 따지자면 유지보수하기 좋은 코드를 만드는 밑바탕이다.

왜냐면 요구사항이 변화하는것에 맞춰 유연하게 변경하니까 가 가장크다.

요즘 자바 공부에 재미를 붙였다.

이렇게 하나하나 습득해서 위로 올라가고 싶다.

나는 아직 자바가 고프다 ㅋㅋㅋㅋㅋㅋ

이번 포스팅 마치도록 하겠다.

728x90

'Java' 카테고리의 다른 글

디자인 패턴 - Bridge Pattern  (0) 2022.08.07
Oauth 이슈  (0) 2022.08.07
연산자  (0) 2022.08.06
[Java] 데이터타입, 변수, 배열  (0) 2022.08.06
728x90

준영속 상태의 지연로딩을 해결하는 방법

JPA에서 항상 생각을 해야되는 것이 바로 영속상태, 영속성 컨텍스트위에 있는가? 를 생각해야된다.

예를 들어 우리는 DAO층에서 실제 DB와 통신을 보편적으로 진행하는데, 이쪽은 영속성 컨텍스트에 의해 관리가 되어
영속 상태를 유지한다.

하지만, Controller, View 이런 계층에서는 준영속 상태가 된다. 그래서 영속상태와 다르게 변경감지, 지연로딩이 동작하지 않게 된다.

지연로딩이 동작하지 않기 때문에 이때 지연 로딩을 시도하면 문제가 발생하는게 당연하다.

하이버네이트가 구현체라면 org.hibernate.LazyInitializationException이 발생한다.

이것을 해결하는 문제는 두가지가 있다.

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

뷰가 필요한 엔티티를 미리 로딩

이름 그대로 영속성 컨텍스트가 살았을 때, 필요한 엔티티를 다 로딩하거나 초기화해서 반환하는 방법이다.

이미 다 로딩했기 떄문에, 지연 로딩이 발생할 걱정을 안해도 된다.

이 방법에서는 로딩해두는 방법에 따라 3가지로 나뉜다.

글로벌 페치 전략 수정

엔티티에서 fetch 타입을 변경하면 애플리케이션 전체에서 이 엔티티 객체를 로드할때 마다 해당 전략을 사용하므로 글로벌 페치 전략이라고 한다.

단점

사용하지 않는 엔티티까지 로딩
N + 1 문제 발생

JPQL을 사용할 때 문제가 발생하는데

Order와 Member가 다대일로 연결되어 있다고 가정한 후에
select o from Order o 를 사용해서 조회를 한다고 한다면
글로벌 페치 전략을 사용하지 않고 그냥 JPQL자체만 사용하기 때문에

  1. 일단 Order를 조회
  2. order 인스턴스들 생성
  3. member 페치 전략이 즉시로딩이므로 order조회되면 member도 조회
  4. 근데 영속성 컨텍스트에 없다? order엔티티 수만큼 계속 조회

이걸 해결하려고 나온것이 바로 아래 부분이다.

JPQL 페치 조인

여기선 join fetch를 사용하면 된다고 한다.

select o from Order o join fetch o.member

N + 1 자체의 문제를 해결해주는것은 좋다. 근데 이제 order만 조회하느냐, order에 연관된 member까지 조회하냐에 따라 메소드를 늘려야할 것이다. 이러면 내부적으로 논리적 의존관계 우려가 발생하기 때문에 잘 고려해서 사용해야 할 것이다.

강제 초기화

지연 로딩을 설정했을 때 연관된 엔티티는 프록시 객체이다.

이 가짜 객체는 실제 사용 시점에 초기화가 되는데 이것을 영속성이 살아있을 때 다 초기화를 하여 반환해준다면? 준영속에서도 사용이 가능하다.

Facade 계층 추가

뷰를 위한 프록시 초기화 담당 계층

서비스와 컨트롤러를 분리해서 그 계층 사이의 의존성을 한번 더 분리해주는 것이라고 생각하면 편하다.

Controller - Facade - Service 이렇게 말이다.

프록시를 초기화하려면 영속성 컨텍스트가 필요해서 Facade에서 트랜잭션을 시작해야 한다.

퍼사드 계층 역할 및 특징

  • 프리젠테이션, 도메인 모델 계층 간의 논리적 의존성 분리
  • 프리젠테이션 계층에서 필요로 하는 프록시 객체 초기화
  • 서비스 계층 호출하여 비즈니스 로직 실행
  • Repository 직접 호출해서 엔티티 탐색

이것도 근데 결국 프리젠테이션 계층에서의 준영속 상태 라는 것이 문제이기 때문에 고안해낸 것.

OSIV

OSIV(Open Session In View)는 영속성 컨텍스트를 뷰까지 열어준다는 뜻.

뷰에서도 지연로딩이 가능❗️

프리젠테이션 계층에서 엔티티를 수정못하게 막는 방법은

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

들이 있다.

스프링 OSIV

True - 기본값

application.properties에 추가해주면 된다.

sping.jpa.open-in-view:true 

스크린샷 2021-08-22 오후 12 43 27

  • 장점

    • 그림과 같이 커넥션을 유지해서 영속성 컨텍스트 범위가 넓어진다.
    • 그래서 연관관계 LAZY fetch 전략을 서비스를 벗어난 곳에서 사용 가능하다.
  • 단점

    • 너무 오랜시간동안 DB 커넥션을 유지하여 리소스를 사용하기 때문에 실시간 트래픽이 중요한 애플리케이션에서 DB 커넥션이 모자랄 수 있음 (장애 발생)

False - 설정값

스크린샷 2021-08-22 오후 12 43 13

sping.jpa.open-in-view : false

DB 커넥션을 Transaction 내부까지만 유지
(트랜잭션은 Service에서 수행되니까 Service까지만 유지되는 것)

  • 장점
    • DB 커넥션 리소스의 효율적인 사용
    • 트랜잭션을 종료할 때 영속성 컨텍스트를 닫으면서 DB 커넥션을 반환
  • 단점
    • 모든 지연로딩을 트랜잭션 안에서 처리
    • 지연로딩에 관한 모든 로직을 Service / Repository 에서 해결해야 함

해결방안

  • Command와 Query를 분리하는 방법 (김영한님 선호)
    • Controller에서 지연로딩을 처리해야 할 때 Query용 Service를 만드는 것
  • OrderService
    • 핵심 비즈니스 로직
  • OrderQueryService
    • 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션)

결론

결국 OSIV는 DB 커넥션 리소스에 대한 효율적인 사용과 관련된 전략이다.

OSIV 실무 TIP

실시간 API 고객 서비스를 해야한다면 OSIV false설정

ADMIN 처럼 커넥션이 많지 않은 곳 OSIV true 설정

728x90

'JPA' 카테고리의 다른 글

고급 주제와 성능 최적화 1  (0) 2022.08.06
컬렉션과 부가기능  (0) 2022.08.06
JPA metamodel must not be empty!  (0) 2022.08.06
[JPA] findAll, findById 차이  (0) 2022.08.06
728x90

삽질기

모든 코드는 깃허브에 있다.

일단 저번에 마이그레이션 1탄을 했었는데 의존성이 겹쳐서 다시 리팩토링 하려고 보니까 에러가 많았다.

의존성 겹침 에러

일단 의존성이 겹쳐서 생긴 에러가 조금 있다.

부분만 적은 gradle 일부이다.

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

dependencies {
    implementation (
            'org.springframework.boot:spring-boot-starter-web', //이부분
            'org.springframework.boot:spring-boot-starter-test', //이부분
            'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0',
            'org.mariadb.jdbc:mariadb-java-client', //이부분
            'commons-io:commons-io:2.6',
            'commons-fileupload:commons-fileupload:1.3.3',
            'javax.servlet:jstl',
            'org.projectlombok:lombok',
    )
}

이렇게 주석처리한 부분들이 에러를 띄웠다.

각자의 버전마다 호응(?)이 되는 버전들이 맞아야 세팅이 완료가 되는데 서로 충돌해서 발생하는 에러였다.

그레이들에서

id 'org.springframework.boot' version '2.5.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'

이 두가지의 부분이 중요한데,

org.springframework.boot는 부트 버전을 명시하여 boot starter 관련 된것들을 통일 시켜주고

dependency-management는 다른 라이브러리들의 의존성을 관리해주는 것인데 최적화된 버전들을 맞춰주는 역할을 한다.

그러면서 버전이 맞지않는게 생기면 에러를 띄워준다.

톰캣 오류

그리고 tomcat 오류였다.

부트와 jsp는 잘 맞지않는다.

그럼에도 일단 jsp가 너무 많아서 일단 jsp를 사용하려고 했기 때문에

의존성에는

implementation (
    'javax.servlet:jstl',
    'org.apache.tomcat.embed:tomcat-embed-jasper'
)

두가지를 일단 추가 해줬다. JSP를 사용하려면 embed-jasper가 존재해야 하는데

이건 기본으로 작성해줘서 문제가 아니었다. 😰

application.properties

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

여기는 둘다 잘 해주었는데.. 왜문제일까 약 2시간 고민했다 😡

근데 여기가 문제였다...

스크린샷 2021-08-21 오후 5 45 32

Working Directory에 저 설정을 안해주면 jsp 파일을 안찾는다. 그냥 반응이 없음..

@Controller @RequestMapping 이런건 문제가 아니었다.

Bean 주입

Bean을 예를 들어

public interface TestInter {
    void hello();
}

@Repository
public class TestImpl implements TestInter {
    @Override
    public void hello() {
        System.out.println("hello");
    }
}

@RequiredArgsConstructor
public class TestService {
    private final TestInter testInter;
}

스프링5 프로그래밍 입문

여기서 봤던건데 스터디원 한분이 알려줬다. 그러면서 책에있는게 기억이 나더라...

결론은 빈을 뭘 주입해야할지 모르는것이다.
@Qualifier를 통해 명시해줘서 하거나 이전 회사에서 하던 방식으로 어노테이션 뒤에 (빈 이름) 해서 매칭해주는 식으로 해야한다. 이게 좋지 않아서 다 삭제했다.

스크린샷 2021-08-21 오후 5 54 26

이랬던 구조들을

스크린샷 2021-08-21 오후 5 54 42

이렇게 바꾸었다. 클래스 이름들은 추후 안정화가 되면 바꿀 계획이다.

학원에서부터 벗어난지 꽤됐고, 그러면서 TDD강의까지 듣고있고 그동안 공부했던거를 생각하면서 코드를 보니까
더러운 부분이 참많다.

이제 시작이고 끝을 보겠다. 계속 리팩토링 하는 과정을 블로그 + 깃허브 에 업데이트 할 것이다.

728x90

'Spring' 카테고리의 다른 글

Filter, Interceptor 정리  (0) 2022.08.07
Jasypt  (0) 2022.08.07
Spring Data JPA  (0) 2022.08.06
Spring -> Spring Boot 마이그레이션  (0) 2022.08.05
728x90

Spring Data JPA

JPA를 사용해도 마찬가지로 DB의 기본적인 CRUD는 어찌됐든 사용하기 마련이다.
근데 이것을 가지고 똑같이 계속 사용해서 엔티티마다 만들어 주는 개념이 아니라
인터페이스에 제네릭 타입을 사용해서 엔티티를 매칭해주어 구현을 하는것이 편리하다.

이 repository를 개발할 때 인터페이스만 정의주면 실행 시점에 Spring Data JPA가 구현 객체를 동적으로 생성해서 주입해준다.
그래서 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발이 가능 하다.

public class MemeberRepository extends JpaRepository<Member, Long> {
    List<Member> findAll();
}

이런식으로 구현을 해준다.
MemberRepository에는 @Repository 를 넣어주지 않는다.

왜냐면

JpaRepository를 상속받은 JpaRepositoryImplementation 를 구현한
SimpleJpaRepository

image
{: text-center}

그림과 같이 @Repository 를 가지고 있다.

그래서 인터페이스만 잘 작성해주면 된다.

제네릭에는 JpaRepository<엔티티, 식별자타입> 이렇게 만들어준다.

JpaRepository의 주요 메서드

  • save() : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
  • delete() : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 를 호출한다.
  • findOne() : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 를 호출한다.
  • getOne() : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 를 호출한다.
  • findAll() : 모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 전달할 수 있다.
  • findBy() : 엔티티 하나를 조회한다. Optional 타입의 객체를 반환한다.

반환 타입

Spring Data JPA는 만약 조회 결과가 없는 경우 컬렉션을 반환하는 메소드에서는 null을 반환한다.

단건으로 두 건 이상 겹쳐서 조회가 될 때 다음의 예제를 보자.

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByName(String name);
}

@DataJpaTest
class MemberTest {
  @Test
  void 두건_조회_테스트() {
      Address address = Address.builder()
          .city("city")
          .street("street")
          .zipcode("zipcode")
          .build();

      Address comAddress = Address.builder()
          .city("cocity")
          .street("costreet")
          .zipcode("cozipcode")
          .build();

      Period period = Period.of("20210714", "20210714");

      Member member = Member.builder()
          .name("name")
          .age(26)
          .period(period)
          .homeAddress(address)
          .companyAddress(comAddress)
          .build();

      Member member2 = Member.builder()
          .name("name")
          .age(26)
          .period(period)
          .homeAddress(address)
          .companyAddress(comAddress)
          .build();

      memberRepository.save(member);
      memberRepository.save(member2);

      memberRepository.findByName("name");
  }
}

같은 삽입 코드를 두개 복사했다. 감안해서 보자.

일단 Member 엔티티인 member, member2를 저장하고 Name 컬럼의 값으로 하나를 가져오는 것인데, 복사해서 붙여넣었으니 같은 값이 즉 2개 이다.
하나의 데이터만 가져오지 못한다면,

image
{: text-center}

NonUniqueResultException 을 발생시킨다.

정확하게 1개 반환하는 것이 아니라면 List로 받는게 오히려 좋을 수도 있겠다.

findById()와 findAll()의 차이를 알고 싶다면

쿼리 메서드

Spring Data JPA가 제공하는 기능이다. 메소드 이름만으로 쿼리를 생성하는 기능이 있는데 인터페이스에 메소드만 선언해주면 해당 메소드 이름으로의 적절한 JPQL 쿼리를 생성해서 날려준다.

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 사용하여 인터페이스에 직접 정의

쿼리 메소드의 규칙에 따라서 정의를 해주어야 동작한다.

쿼리 메소드 규칙

키워드 JQPL 예
And findByNameAndAddress where x.Name = ?1 and x.Address = ?2
Or findByNameOrAddress where x.Name = ?1 or x.Address = ?2
Distinct findDistinctByLastnameAndFirstname select distinct … where x.lastname = ?1 and x.firstname = ?2
Is、Equals findByFirstname, findByFirstnameIs, findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age <= ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull、Null findByAge(Is)Null … where x.age is null
IsNotNull、NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 ( %가 뒤에 추가된 매개 변수)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 ( %가 앞에 추가된 매개 변수)
Containing findByFirstnameContaining … where x.firstname like ?1 ( %가 래핑된 매개 변수)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection ages) … where x.age not in ?1
True findByActiveTrue() … where x.active = true
False findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstname) = UPPER(?1)

NamedQuery

보통 @NamedQuery 로 정의하는 방식인데, 이거는 MyBatis를 쓰는 방식같은 느낌이 많이 든다.

이것은 전의 포스팅을 보면 이해가 쉬울 수 있다.

@Query, Repository 메소드에 쿼리 정의

이 방법이 더 간단하고 편리하게 정의할 수 있는 느낌이 더 강하다.

이름없는 NamedQuery 라고 생각하면 좋을듯? 싶다 😁

장점이라고 하면 애플리케이션 실행 시점에 문법 오류를 발견할 수 있어서 좋다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Memer m where m.name = ?1")
    Member findByName(String name);
}

사용은 이런식으로 하면 되겠다.

Native SQL을 사용하려면 @Query("sql", nativeQuery=true) 로 설정하면 되고 이 네이티브 쿼리에서는 파라미터 바인딩 숫자가 0부터 시작한다.

0부터 시작하면 뭐하나 인덱스 밀려서 쓰면 값 에러 날 수 있으니 권장하지 않는다.

위치 기준 파리미터 주의점

위치(순서) 기준보다는 이름 기준의 바인딩을 추천한다. 위치 기준의 바인딩을 사용하면 추후에 요소가 추가되면 밀리거나 땡겨질 수가 있기 때문❗️

그래서 아래와 같은 방법을 더 많이 쓴다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Memer m where m.name = :name")
    Member findByName(@Param("name") String name);
}

정렬

Spring Data JPA에는 쿼리 메소드에 페이징 그리고 정렬기능을 사용하도록 2가지 파라미터를 제공한다.

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort) 포함

여기서 정렬 실습에 대한 에러도 같이 남긴다.

우선 sql DDL 그리고 DML 이다.

member.sql

CREATE TABLE IF NOT EXISTS MEMBER
(   ID              BIGINT GENERATED BY DEFAULT AS IDENTITY,
    NAME            VARCHAR(255),
    AGE             INTEGER NOT NULL,
    COMPANY_CITY    VARCHAR(255),
    COMPANY_STREET  VARCHAR(255),
    COMPANY_ZIPCODE VARCHAR(255),
    CITY            VARCHAR(255),
    STREET          VARCHAR(255),
    ZIPCODE         VARCHAR(255),
    END_DATE        VARCHAR(255),
    START_DATE      VARCHAR(255),
    PRIMARY KEY (ID)
);

INSERT INTO MEMBER (name, age, company_city, company_street, company_zipcode, city, street, zipcode, end_date, start_date) VALUES ('테스트', 33, 'city1', 'street', 'zipcode', 'cocity1', 'costreet', 'cozipcode', '20210714', '20210714');
같은 쿼리 여러개 넣었다고 생각하자.
...
...
...

처음에 이렇게 쿼리메소드를 구현하였다.🙂

public interface MemberRepository extends JpaRepository {
    //기존 로직
    ...

    Page<Member> findByName(String name, Pageable pageable);
}

그리고 테스트코드에서는...

Page<Member> members = memberRepository.findByName("테스트", PageRequest.of(0, 3, Sort.by(Order.desc("age"))));

이렇게 Page<T>를 사용하면 페이징 처리를 할 수있다.

첫 페이지는 0부터 시작한다.

이슈 발생

여기서 생긴 이슈는 분명 Sort.by(Order.desc("정렬할 컬럼")); 으로 정렬을 넣어줬음에도 쿼리에서는 정렬을 해주지 않았고 근데 정렬방식은 DESC로 나오는 경우였다.

findByName
{: text-center}

result
{: text-center}

이렇게 페이징을 위해 count쿼리는 나온다. 근데 where 이후로 order by 가 없는 것이다.

스터디를 하는 분과 같이 의논을 Code With Me로 같이 삽질을 좀 했다.

p.s 시간 내주셔서 감사합니다!

결과 알아낸 것은 내가 쿼리 메소드로 구현한건 findBy 로 시작하는 쿼리메소드를 오버로딩해서 Pageable만 추가해줬다.

이렇게 하니까 결국 기본으로 Spring Data JPA에서 정의된 쿼리메소드 참조를 하기 때문에 우선순위가 그쪽으로 배정되어서 조회는 ASC로 되는가보다.

해결책

public interface MemberRepository extend JpaRepository {
    Page<Member> findMembersByName (String name, Pageable pageable);
}

보다시피 메서드 이름을 findMembersByName 으로 바꿨고,

@DataJpaTest
@ActiveProfiles("test")
public class DbTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    @Sql("classpath:member.sql")
    void dbTest() throws Exception {
        Page<Member> members = memberRepository.findMembersByName("테스트", PageRequest.of(0, 3, Sort.by(Order.desc("age"))));


        Pageable pageable = members.getPageable();

        System.out.println("pageable = " + pageable);
        System.out.println("pageable.getSort() = " + pageable.getSort());
    }
}

이렇게 해주니까

findMembersByName
{: text-center}

잘 나오게 된다.

2021-08-22 수정본

위 부분은 사실 단순한 해결이었고, 진정한 해결책은 우선순위는 @NamedQuery 에 있다.

실습때문에 Member 엔티티에 NamedQuery를 추가했었어서 이 부분이 먼저 구현되어 비정상적으로 정렬이 됐던 것이다. 스터디원들 다 같이 모여 해결했다. 이럴때 집단지성이 굉장히 좋은것 같다👍

아무튼 구문자체에는 오류가 없었고 네임드 쿼리 쓰지말자! Mybatis를 보면 볼 수록 생각나게 한다. 이제 정을 떼는것이 좋을것 같다.

좀 더 편리하게 사용하려면 interface다중상속이 가능하기 때문에
이렇게 상속해서 사용해도 될 것 같다.

이상으로 12장 포스팅을 마치도록 하겠다.

728x90

'Spring' 카테고리의 다른 글

Jasypt  (0) 2022.08.07
Spring -> Spring Boot 마이그레이션 2  (0) 2022.08.06
Spring -> Spring Boot 마이그레이션  (0) 2022.08.05
[Spring] MockMvc Bean 주입 에러  (0) 2022.08.04
728x90

연산자, 조건문 정리

  • 산술 연산자
  • 비트 연산자
  • 관계 연산자
  • 논리 연산자
  • instanceof
  • assignment(=) operator
  • 화살표(->) 연산자
  • 3항 연산자
  • 연산자 우선 순위
  • (optional) Java 13. switch 연산자

산술 연산자(Arithmetic Operator)

산술 연산자는 사칙연산을 다루는 연산자로, 가장 기본적이면서 가장 많이 사용되는 연산자 중의 하나.

산술 연산자는 모두 두 개의 피연산자를 가지는 이항 연산자이며, 결합 방향은 왼쪽에서 오른쪽이다.

연산자 설명
+ 더하기 연산 수행(문자열 연결 가능)
- 마이너스 연산 수행
* 곱하기 연산 수행
/ 나누기 연산 수행(정수형은 몫 연산자)
% 나머지 연산 수행
int result = 1 + 2;
System.out.println(result);

String ss = "연습";
System.out.println(ss + " 입니다.");

int product = 5 * 4;
System.out.println(product);

System.out.println("정수형 나누기");
System.out.println(5 / 4);
System.out.println(5 % 4);

System.out.println("실수형 나누기");
System.out.println(5.0 / 4.0);
System.out.println(5.0 % 4.0);


System.out.println(5 / 0); // java.lang.ArithmeticException: / by zero

System.out.println(5.0 / 0.0); // Infinity

System.out.println(5.0 % 0.0); // NaN

System.out.println(-5.0 / 0.0); // -Infinity

System.out.println(-5.0 % 0.0); // NaN

정수형일때 조심해야 하는 점은 보는 것과 같이 나눗셈 연산을 수행할 때이다.
그냥 실행이 잘 되기 때문에 % 연산을 수행할 때에도 음수값으로 나눌때 값이 다르다.

실수형일때는 연산 과정에서는 오차가 발생할 수 있고 이것을 조심해야 한다.
0으로 나누거나 나머지 연산을 사용하면 Infinity, NaN 가 나온다.

단항 연산자

  • + 를 붙이면 양수를 나타냄. (생략 가능)
  • - 를 붙이면 음수 값을 나타낸다.

증감 연산자

++를 붙이면 값을 1씩 증가시킨다. 하지만 피연산자의 위치에 따라 계산이 살짝 달라진다.

앞쪽에 ++변수 의경우 식을 진행하기 전에 1을 증가시키고, 뒤쪽변수++에 위치하는 경우 식을 진행한 후에 1을 증가시킨다.

-- 도 이와 같다.

int result = 1;
for (int i = 0; i < 5; i++) {
    System.out.print(result++ + " ");
}

System.out.println();
int result2 = 1;

for (int i = 0; i < 5; i++) {
    System.out.print(++result2 + " ");
}

image

두 증감 연산자에 대한 차이이다.

비트 연산자 (Bitwise Operator)

논리 연산자와 비슷하지만, 비트 단위로 논리 연산을 할 때 사용하는 연산자이다.

비트 단위로 왼쪽이나 오른쪽으로 전체 비트를 이동하거나 1의 보수를 만들 때도 사용된다.

연산자 설명
& 대응되는 비트가 모두 1이면 1을 반환함.(AND 연산)
대응되는 비트중 하나라도 1이면 1을 반환함.(OR 연산)
^ 대응되는 비트가 서로 다르면 1을 반환함.(XOR 연산)
~ 비트를 1이면 0으로, 0이면 1로 바꿈(NOT 연산, 1의 보수)
<< 명시된 수 만큼 비트를 전부 왼쪽으로 이동(left shift 연산)
>> 부호를 유지하면서 지정한 수 만큼 전부 오른쪽으로 이동(right shift 연산)
>>> 지정한 수만큼 비트를 전부 오른쪽으로 이동시키며, 새로운 비트는 전부 0이 됨.

관계 연산자

관계 연산자는 피연산자가 값이 어떤지 비교하는 연산자들이다.

  • == : 같다.
  • != : 다르다.
  • > : 크다
  • >= : 크거나 같다.
  • < : 작다.
  • <= : 작거나 같다.

문자열이 같은지를 보려면 equals()를 사용해야 한다.

==은 주소값을 비교하는 것이기 때문에 String 은 equals를 사용하자.

논리 연산자

피연산자 값으로 boolean을 받고 true/false를 판단할 경우에 사용한다.

  • &&
    • AND 연산자이며, 좌우 값이 둘다 참일 경우에 true, 그렇지 않으면 false를 반환
  • ||
    • OR 연산자로, 한쪽만 참이어도 참을 반환한다.
  • !
    • NOT 연산자로 위의 두개와는 다르게 단항 연산자이며 조건이 거짓일때 true값을 반환해준다. 그러니까 기본이 거짓이면 true, 참이면 false를 반환한다.

instanceOf

객체 타입을 확인하는데 주로 사용하고, 나같은 경우에는 테스트 코드를 작성했을 때, 많이 사용한 메서드이다. 속성 자체는 연산자이고, 형 변환이 가능한지 여부를 true/false 로 가르쳐준다. 부모, 자식 객체인지 확인하는데 쓴다고 생각하면 된다.

class A {

}

class B extends A {

}

public class InstanceTest {
  public static void main(String[] args) {
    A a = new A();
    B b = new B();

    System.out.println(a instanceof A);
    System.out.println(b instanceof A);
    System.out.println(a instanceof B);
    System.out.println(b instanceof B);
  }
}

결과는 true, true, false, true 가 나온다.

b는 부모 클래스인 a를 상속받았기 때문에 A의 객체타입이 맞다.

그래서 true이고 A클래스는 반대로 B에 대한 부모 클래스이기 때문에 B의 객체 타입일 수는 없으므로 false를 출력하게 된다.

assignment(=) operator

할당 연산자는 연산자 기준 오른쪽값을 왼쪽의 피연산자에게 할당한다.

int x = 0; 에서 x라는 int형 변수에 0을 할당하는 것이다.

A a = new A(); 처럼 객체를 할당할 수도 있다.

복합 할당

public class Test {
  public static void main(String[] args) {
      int a = 0;
      a = a + 1;
      a += 1;
  }
}

a = a + 1 의 경우

image1

a += 1 의 경우

image2

서로 바이트 코드가 다르다!!!

아래의 경우가 속도가 조금 빠르지 않을까 생각한다.

화살표(->) 연산자

화살표 연산자는 Java8에서 람다 표현식으로 익명클래스를 대체한 것이다.

public interface Test {
    void test();
}

public class Main {
    public static void main(String[] args){
        Test test = new Test() {
            @Override
            public void test() {
                //로직 구현
            }
        };
    }
}

이런식으로 익명 클래스를 사용하여 구현을 해줬었다.

근데 이 화살표 연산자가 생긴뒤로는 이렇게 구현하지 않고 아래와 같이 구현한다.

@FunctionalInterface
public interface Test {
    void test();
}

Test test = () -> 로직;

이렇게 구현을 해준다.

@FunctionalInterface 를 인터페이스에 붙여주게 되면 interface에는 하나의 추상메서드만 정의가 가능하다.

람다를 다시 공부할때 포스팅 하도록 하겠다.

3항 연산자

삼항 연산자는 조건 ? 참일 경우 : 거짓일 경우 로 구현하는데 피연산자를 세개를 받으므로 삼항 연산자라고 불린다.

if-else문과 비슷한 역할을 수행하지만 한줄로 작성이 가능하다.

public class Ternary {
    public static void main(String[] args) {
        int n1 = 5;
        int n2 = 10;

        int max = (n1 > n2) ? n1 : n2;
        System.out.println("max : " + max);
    }
}

기본적으로 연산자에는 우선순위가 있으며, 괄호의 우선순위가 제일 높고,

산술 > 비교 > 논리 > 대입의 순서이며, 단항 > 이항 > 삼항의 순서다.

연산자의 연산 진행방향은 왼쪽에서 오른쪽으로 수행되며,

단항 연산자와 대입 연산자의 경우에는 오른쪽에서 왼쪽으로 수행된다.

image3

switch 연산자

어떠한 값이 맞는다면 해당하는 식을 수행하게끔 만든 연산자이다.

단일 값으로 평가되는 하나의 표현식.

java 15 버전에서는 case를 case -> 로 표현한다고 한다.

public class Main {
    public static void main(String[] args) {
        String day = "월";

        switch (day) {
            case "월":
                System.out.println("월요일");
                break;
            case "화":
                System.out.println("화요일");
                break;
            case "수":
                System.out.println("수요일");
                break;
            case "목":
                System.out.println("목요일");
                break;
            case "금":
                System.out.println("금요일");
                break;
        }
    }
}

이것을

switch (day) {
    case "월" -> System.out.println("월요일");
    case "화" -> System.out.println("화요일");
    case "수" -> System.out.println("수요일");
    case "목" -> System.out.println("목요일");
    case "금" -> System.out.println("금요일");
    case "토", "일" -> System.out.println("주말");
}

이렇게 표현이 가능하다.

if-else문은 원하는 조건이 나올때까지 순서대로 모든 경우를 비교하고

switch문은 jump-table을 사용해서 한번에 원하는 곳에 이동한다.

그래서 if문은 조건문의 개수만큼 O(n)의 시간복잡도를 갖게 되어 성능에 단점이 있고,

switch문은 case의 개수만큼 jump-table을 차지하므로 메모리에 단점이 있다.

때문에 성능면으로 보면 switch문이 더 빨라서

조건이 3개 이상일 경우에는 switch를 사용하는 것이 더 좋다고 한다.

그렇지만 사실 그 차이는 컴파일러의 처리 속도에 따라 차이가 생기는 것이고

요즘 컴파일러들이 워낙 우수하기 때문에 차이가 미비하다고 하다.

if-else 를 쓰든 switch 를 쓰든 (특별히 성능과 메모리의 이슈를 갖고 있지 않은 이상) 각자의 취향이 아닐까.

가독성을 높이는 쪽으로 선택해서 사용하면 될 것 같고

나는 되도록이면 Early Return을 하려고 한다.

String string;

if (조건) {
    string = "참";
} else {
    string = "거짓";
}

이런게 있다고 하면 조건에 부합하는 문자열만 보내주면 된다.

그래서

String string = getString();

public String getString() {
    if (조건) {
        return "참";
    }

    return "거짓";
}

이렇게 분리하는 방법이 좋은것 같다.

이걸 더 분리한다고 치면 이제 공부했던 State패턴을 사용하면 되겠다.

이거는 다시 정리하는것이기 때문에 나중에 객체지향 개념을 들어가게 되면 구체적으로 정리를 해보도록 하겠다.

728x90

'Java' 카테고리의 다른 글

Oauth 이슈  (0) 2022.08.07
SOLID 원칙  (0) 2022.08.06
[Java] 데이터타입, 변수, 배열  (0) 2022.08.06
JVM  (0) 2022.08.06

+ Recent posts