H2 데이터베이스는 이미 설치했기 때문에 넘어가도록 하겠습니다.

 

아래 링크에 H2 데이터베이스 설치했던 내용이 담겨있는 내용이 있습니다.

 

[JPA 활용 1] 1. 프로젝트 환경설정

프로젝트 환경설정 프로젝트 생성 라이브러리 살펴보기 View 환경 설정 H2 데이터베이스 설치 JPA와 DB 설정, 동작확인 프로젝트 생성 스프링 부트 스타터(https://start.spring.io/) 사용 기능 : web, thymelea

qazyj.tistory.com

 

 

 메이븐 소개

  • https://maven.apache.org/
  • 자바 라이브러리, 빌드 관리
  • 라이브러리 자동 다운로드 및 의존성 관리
  • 최근에는 그레들(Gradle)이 점점 유명 (저도 그레들로 많이 사용했습니다.)

 

 

프로젝트 생성

  • 자바 8 이상(8 권장)
  • 메이븐 설정
    • groupId : jpa-basic
    • artifactId : ex1-hello-jpa
    • version : 1.0.0

intelliJ를 킨 후 new Project에서 maven을 선택한 뒤 위의 그림대로 작성 후 프로젝트를 생성해주시면 됩니다.

 

코드추가 (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>jpa-basic</groupId>
    <artifactId>ex1-hello-jpa</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <!-- JPA 하이버네이트 -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.3.10.Final</version>
        </dependency>
        <!-- H2 데이터베이스 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>
    </dependencies>
</project>

 

 

JPA 설정하기 - persistence.xml

jpa를 쓰기위해선 몇가지 설정을 해야합니다.
  • JPA 설정 파일
  • /META-INF/persistence.xml 위치 (표준 위치가 정해져 있습니다.)
  • persistence-unit name으로 이름 지정 (JPA이름, DB마다 만든다고 합니다.)
  • javax.persistence로 시작 : JPA 표준 속성
  • hibernate로 시작 : 하이버네이트 전용 속성

 

코드 추가 (METE-INF/persistence.xml)

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <!--<property name="hibernate.hbm2ddl.auto" value="create" />-->
        </properties>
    </persistence-unit>
</persistence>

 

 

데이터베이스 방언

  • JPA는 특정 데이터베이스에 종속 X
  • 각각의 데이터베이스가 제공하는 SQL 문법과 함수는 조금씩 다름
    • 가변 문자 : MySQL은 VARCHAR, Oracle은 VARCHAR2
    • 문자열을 자르는 함수 : SQL 표준은 SUBSTRING(), Oracle은 SUBSTR()
    • 페이징 : MySQL은 LINIT, Oracle은 ROWNUM
  • 방언 : SQL 표준을 지키지 않는 특정 데이터베이스만의 고유한 기능
  • hibernate.dialect 속성에 지정
    • H2 : org.hibernate.dialect.H2Dialect
    • Oracle 10g : org.hibernate.dialect..Oracle10gDialect
    • MySQL : org.hibernate.dialect.MySQL5InnoDBDialect
  • 하이버네이트는 40가지 이상의 데이터베이스 방언 지원

 

애플리케이션 개발

 

JPA 구동 방식

  1. Persistence라는 클래스를 통해 설정 정보를 조회합니다.
  2. 설정 정보를 조회해서 EntityManagerFactory라는 클래스를 만듭니다. 
  3. EntityManagerFactory 클래스에서 필요할 때마다 EntityManager를 찍어냅니다.

 

 

실습 - JPA 동작 확인

  • JpaMain 클래스 생성
  • JPA 동작 확인

 

 

객체와 테이블을 생성하고 매핑하기

  • @Entity
    • JPA가 관리할 객체
  • @Id
    • 데이터베이스 PK와 매핑

localhost:8082를 url로 검색한 뒤, JDBC URL에 persistence.xml에 입력했던 아래의 코드의 value값을 넣어야지 정상적으로 연결이 됩니다.

<property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>

Entity 클래스를 만들고, 트랜잭션을 보내주면 log로 쿼리문을 볼 수 있습니다. 위와 같은 쿼리문이 로그에 출력된다면 정상적으로 jpa가 작동하는 것 입니다. 쿼리문이 로그로 출력이 되는 이유는 persistence.xml 코드에서 옵션 첫번째 줄에 show_sql의 value값을 true로 주었기 때문입니다. format_sql은 로그를 위와같이 가독성이 좋게 출력을 해줍니다. use_sql_comments주석된 부분을 로그로 보여줄 것인지 아닌지를 설정해줍니다. h2 콘솔로 확인해보면 입력했던 데이터가 저장된 것을 확인할 수 있습니다.

트랜잭션 정석코드로 변경했습니다. 트랜잭션을 보냈을 때, 에러가 발생할 시 rollback하고 정상적으로 종료되면 EntityManager와 EntityManagerFactory를 종료합니다. 지금은 jpa 정석 코드를 보여주기 위해 적었지만 실제로는 spring 위의 코드가 다 없어지고 em.persist(member)만 적으면 작업을 완료할 수 있습니다. 

 

JPA 조회

조회는 위와 같이 find 메소드를 통해 찾을 수 있습니다. 파라미터로 엔티티 클래스명, PK를 넘겨주면 DB에 저장한 데이터를 가져올 수 있습니다. 가져온 Entity의 값을 출력해보면 이전에 넣었던 데이터가 출력되는 것을 볼 수 있습니다. 

 

JPA 삭제

삭제는 위처럼 remove에 찾은 Entity 클래스 필드를 넣어주면 됩니다. 

 

JPA 수정 (신기함)

수정이 진짜 신기합니다. 마치 자바 컬렉션을 사용하는 것처럼 사용만하면 수정이됩니다. 단지, setter를 활용해 데이터를 수정만해준 뒤 트랜잭션만 보내면 JPA가 자동으로 DB 데이터를 수정해줍니다. 이것이 가능한 이유는 JPA를 통해 Entity를 가져오면 해당 Entity는 JPA가 관리하기 때문입니다. 그리고 JPA가 변경이 되는지 안되는지 트랜잭션을 commit하는 시점에 체크합니다. 그 후 바뀐 데이터가 있을 시 업데이터 쿼리를 작성하여 DB에 날려줍니다. 

 

 

주의

  • EntityManagerFactory는 하나만 생성해서 애플리케이션 전체에서 공유
    • 웹 애플리케이션 서비스를 실행한다고하면 EntityManagerFactory는 DB 당 하나만 생성한다고 보면 됩니다.
  • EntityManager는 쓰레드간에 공유 X (사용하고 버려야 한다.)
    • 고객의 요청에 따라서 사용한 뒤 close, 사용한 뒤 close 반복한다고 보면 됩니다.
    • 이유는 후에 이해하면 정리하도록!
  • JPA의 모든 데이터 변경은 트랙잭션 안에서 실행
    • 정말 중요합니다!
    • RDB 데이터 변경은 트랜잭션안에서 할 수 있도록 설계가 되어있기 때문입니다.

 

 

JPQL 소개

  • 가장 단순한 조회 방법
    • EntityManager.find(Entity 클래스명, PK 값)
    • 객체 그래프 탐색 (a.getB().getC())
  • 나이가 18살 이상인 회원을 모두 검색하고 싶다면?
    • JPQL을 사용해야 합니다.

현업에서의 개발고민은 여러 테이블이 정말 많은데 필요하면 조인도해야하고, 원하는 데이터를 최적화 해서 가져와야하고, 필요하면 통계성 쿼리도 날려야하고 이러한 작업들을 JPA에서는 JPQL로 도와준다고 보면 됩니다. 쿼리를 어떻게 쓰지라는 고민이 드실텐데 JPA에서는 대안이 있다고 합니다.

 

 

실습 - JPQL 소개

  • JPQL로 전체 회원 탐색
  • JPQL로 ID가 2 이상인 회원만 검색
    • 쿼리문에 where m.id =
  • JPQL로 이름이 같은 회원만 검색
  • JPQL에 대해 자세한 내용은 객체지향 쿼리에서 학습

 

JPQL로 전체 회원 탐색

log를 잘 보면 실제 전송한 쿼리랑 다르게 모든 필드를 나열한 것을 볼 수 있습니다. 

 

 

JPQL

  • JPA를 사용하면 엔티티 객체 중심으로 개발할 수 있다.
  • 문제는 검색 쿼리
  • 검색을 할 때도 테이블이 아닌 엔티티 객체로 탐색
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
  • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
  • JPQL은 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공
    • SQL을 추상화했기 때문에 특정 데이터베이스에 의존 X
  • SQL 문법과 유사, SELECT FROM, GROUP BY, WHERE, HAVING, JOIN 지원
  • JPQL은 엔티티 객체를 대상으로 쿼리
  • SQL은 데이터베이스 테이블을 대상으로 쿼리
  • JPQL은 뒤에서 아주 자세히 다룸

 

 

 

 

 

 

 

 

 

 

 

 

 

출처

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

강의 정리를 시작하기 앞서

왜?? 라는 질문에 대답하기 위해선 해당 분야에 많은 지식이 있어야지 대답을 해줄 수 있습니다.'
앞으로 스스로에게 왜?? 라는 질문을 던지며 정리를 하려고합니다. 왜?? 라는 질문은 강의와 책을 보며 찾은 해답을 정리할 것이고, 아직 해답을 못찾은 왜?라는 질문에 대한 대답은 후에 정리하도록 하겠습니다.

 

JPA 왜 공부해야 하는가?

1. 현재 JPA를 공부하는 이유는 점유율입니다. JPA, Mybatis 등 중에서 세계적으로 앞도적인 점유율을 보여주는 JPA를 사용할 줄 알고, 이해하고 있어야지 취업에 도움이 될 것이라고 생각하기 때문입니다.

2. 무한 반복, 지루한 코드를 적어야하는 SQL의 문제점을 보완해주기 때문입니다. (간단하게 쿼리문 작성이 필요하지 않습니다.) 이에따라서, 개발 시간이 줄어듬으로써 설계나 테스트 등에 시간을 더 사용할 수 있습니다.

3. 시간을 줄여준다고 성능이 입증되지 않은 것도 아닙니다. 현재 조 단위의 거래 금액이 발생하는 다양한 서비스에서 사용하고 있으며 문제없이 사용되기 때문에 문제없다고 생각했습니다.

 

SQL 중심적인 개발의 문제점

 

무한 반복, 지루한 코드를 적어야하는 예를 한번 들어봅시다. JPA가 없는 SQL만으로 작성된 아래의 코드로 작성된 프로젝트가 있다고 가정해봅시다.

여기서, 기획자에 요청에 의해서 객체에 번호 필드를 추가해야되는 상황이 주어졌습니다. 객체에 tel이란 필드를 생성하면 끝나는 것이 아닌 모든 쿼리문에 tel을 추가해야되는 상황이 발생합니다. 

결국, 관계형 데이터베이스를 사용하는 환경에서는 SQL에 의존적인 개발을 피하기 어렵습니다.

 

그리고 단순한 SQL 문을 작성하는 문제를 넘어서서 또 하나의 문제점이 있습니다. 바로 패러다임의 불일치 입니다.

 

 

패러다임의 불일치

객체 vs 관계형 데이터베이스

관계형 데이터베이스의 사상과 객체 지향 사상이 다릅니다.

 

객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공합니다. 반면, 관계형 데이터베이스는 데이터를 잘 정규화해서 보관하는 것을 목표로합니다.

 

하지만 생각을 뒤집어 봅시다. 먼저 객체를 잘 정규화해서 저장한다고 봅시다. 선택지는 RDB, NoSQL, File 등 여러가지가 있습니다. 여기서 데이터를 저장하는 다양한 방법 중 현실적인 대안은 RDB입니다. 이유는 데이터가 백만건 있다고 할 때 파일에 집어 넣는 경우는 검색이 불가능하고 백만건 모두를 Object를 만든 뒤 검색하기에는 답이 없기 때문입니다. NoSQL도 대안이 될 수 있기도 하지만 아직까지 Main은 RDB를 쓰기 때문에 힘들다고 합니다. 그렇기 때문에 RDB를 사용해야합니다.

결론적으로, RDB를 사용함으로써 객체를 SQL로 바꿔야하기 때문에 SQL은 필수로 사용해야 합니다. 이러한 작업은 개발자가 해야합니다. 

 

 

객체와 관계형 데이터베이스의 차이

  1. 상속
    • 객체 : 상속 관계 O
    • RDB : 상속 관계 X
  2. 연관관계
    • 객체 : 레퍼런스 ex) ArrayList에서의 get
    • RDB : PK, FK
  3. 데이터 타입
  4. 데이터 식별 방법

 

상속

위와 같은 상속 관계가 있다고 가정해봅시다.

 

삽입

  1. 객체 분해
  2. INSERT INTO ITEM ...
  3. INSERT INTO ALBUM ...

insert의 경우 3번의 작업을 거치긴하지만 가능합니다.

 

조회

  1. 각각의 테이블에 따른 조인 SQL 작성 ..
  2. 각각의 객체 생성 ..
  3. 상상만 해도 복잡합니다.

그렇기때문에 DB에 저장할 객체에는 상속 관계를 쓰지 않습니다.

 

하지만, DB가 아닌 자바 컬렉션에 저장한다면 어떻게 될까요??

삽입

list.add(album);

조회

Album album = list.get(albumId);

// 부모 타입으로 조회 후 다형성 활용
Item item = list.get(albumID);

 자바 컬렉션에 넣으면 심플해지지만, RDB에 넣고 빼는 순간 중간에 SQL 매핑 작업을 개발자가 한땀한땀 해야합니다.

 

 

연관관계

  • 객체는 참조를 사용 : member.getTeam()
  • 테이블은 외래 키를 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID

객체의 경우 Member에서 Team을 찾을 수 있고, Team에서 Member를 찾을 순 없습니다. 하지만 테이블의 경우 MEMBER에서 TEAM을 join할 수 있고, TEAM에서 MEMBER를 join할 수 도 있습니다.

 

객체를 테이블에 맞추어 모델링

class Member {
    String id;		// MEMBER_ID 컬럼 사용
    Long teamId;	// TEAM_ID FK 컬럼 사용 // **
    String username;// USERNAME 컬럼 사용
}

class Team {
    Long id;		// TEAM_ID PK 사용
    String name;	// NAME 컬럼 사용
}

 

테이블에 맞춘 객체 저장

하지만 이는 객체다운 모델링 같지 않습니다. Member 클래스에 있는 teamId의 타입이 Team이 아닌 Long으로 되어있기 때문입니다. 

 

객체다운 모델링

class Member {
    String id;		// MEMBER_ID 컬럼 사용
    Team teamId;	// 참조로 연관관계를 맺는다. // **
    String username;// USERNAME 컬럼 사용
    
    Team getTeam() {
    	return team;
    }
}

class Team {
    Long id;		// TEAM_ID PK 사용
    String name;	// NAME 컬럼 사용
}

 

객체 모델링 저장

이렇게 설계하면 정상적으로 저장이 가능합니다. 하지만 조회할 때 문제가 생깁니다.

 

 

객체 모델링 조회

위와 같이 주석처리 된 부분의 코드가 상당할 것으로 예상됩니다. 결론적으로 매우 번거롭습니다.

 

 

객체 모델링, 자바 컬렉션에 관리 

훨씬 편리해진 것을 볼 수 있습니다. 

 

 

객체 그래프 탐색

객체는 자유롭게 객체 그래프를 탐색할 수 있어야 합니다. 아래의 그래프를 예시로 들어 봅시다.

여기서 자유롭게 그래프를 탐색한다는 것은 자바에서 get을 통해 모든 객체로 갈 수 있다는 것을 의미합니다.

 



처음 실행하는 SQL에 따라 탐색 범위 결정

이처럼 SELECT로 MEMBER와 TEAM을 가져오면 getTeam은 가능하지만, getOrder은 불가능합니다. 이것을 해결하기 위해선 일일이 코드를 다 살펴봐야하는 문제가 발생하게 됩니다. 즉, 엔티티에 신뢰할수 없다는 것 입니다.

 

 

엔티티 신뢰 문제

위에 코드가 있다고 가정합시다. Member를 받아와서 getTeam(), getOrder().getDelivery()를 수행하려 합니다. 하지만 엔티티를 모두 가져올 수 있을지는 장담할 수 없습니다. 이를 알아보기 위해선 Member를 가져올 때 TEAM, ORDER, DELIVERY 데이터를 모두 긁어 오는지를 확인해야 합니다.

 

 

모든 객체를 미리 로딩할 수는 없다.

그렇다면 모든 객체를 미리 모두 로딩해놓으면 되는 것 아닌가라는 의문점이 생깁니다. 하지만 사실상 현업에서 사전에 미리 로딩하는 작업을 하게되면 서비스를 이용하기 직전에 불필요한 데이터까지 조회를 하기때문에 너무 오랜 시간을 기다릴 수 있어 서비스적인 측면에서 좋지 못합니다. (개인적인 생각입니다.) 그렇기때문에 위와같이 테이블을 조회하는 모든 경우의 수를 메소드화 시키는 것이 좋지만 이또한 코드가 지저분해지고 지루한 코드를 오랜시간 작성해야하는 번거로움이 있습니다. 

 

이렇듯 SQL을 직접 다루게 되면 계층형 아키텍처에서 진정한 의미의 계층 분할이 어렵습니다. (진정한 게층 분할은 뭘까??)

 

 

비교하기

SQL에서 조회

member1과 member2가 식별자는 똑같지만 다른 이유는 다른 쿼리문으로 받아온 값을 new Member를 통해 반환하기 때문에 당연히 다릅니다. 그렇다면 자바 컬렉션에서는 어떨까요?

자바 컬렉션에서 조회

이것은 당연하게 같은 참조값으로 조회하기때문에 같은 Member를 반환합니다.

 

이처럼 자바 컬렉션에서 객체를 다룰 때와 SQL(RDB)에서 객체를 다룰 때와 중간에 많은 미스매치가 나는 것을 볼 수 있습니다. 그렇기때문에 RDB를 객체답게 모델링 할수록 매핑 작업만 늘어나게 됩니다.

 

이러한 무한 반복, 지루한 코드를 계속해서 작성하던 선배 개발자분들은 "객체를 자바 컬렉션에 저장 하듯이 DB에 저장"하고 싶어집니다. 이러한 고민의 결과가 바로 JPA이고, 공부를 해야하는 이유 입니다.

 

 

JPA 소개

JPA란?

Java Persistence API의 약자로 자바 진영의 ORM 기술 표준입니다. 

 

ORM이란?

Object-Relational Mapping (객체 관계 매핑)의 약자로 객체는 객체대로 설계, 관계형 데이터베이스는 관계형 데이터베이스대로 설계하는 것을 의미합니다. 각각 설계된 객체와 RDB를 ORM 프레임워크가 중간에서 매핑합니다.

 

 

JPA 동작

JPA는 JAVA 애플리케이션과 JDBC 사이에서 동작합니다. 그렇기때문에 개발자가 직접 JDBC API를 쓰는 것이 아닌 JPA에게 명령하면 JPA가 JDBC API를 사용해서 SQL을 호출하고 만들어서 보내고 결과를 받는 등의 동작을 하게 됩니다.

 

 

JPA 동작 - 저장

예를들어, MemberDAO에서 객체를 저장하고 싶다고 가정해봅시다. 과거에는 JDBC API나 JDBC Template, Mybatis를 썼다면 JPA를 사용하게 될 경우에는 JPA에게 Member 객체를 넘깁니다. 그렇게되면 JPA가 Member 객체를 분석하고 적절한 INSERT SQL을 생성하고 JDBC API를 사용해서 쿼리를 DB에 보냅니다. 여기서 JPA의 장점은 쿼리를 개발자가 작성하는 것이 아닌 JPA가 작성해줍니다. 그렇기때문에 패러다임의 불일치가 해결됩니다. (아직까지는 어떻게 패러다임의 불일치가 해결되는지 이해가지 않지만 뒤에서 해결해준다고합니다.)

 

JPA 동작 - 조회

조회할 때도 마찬가지로 JPA에게 PK값만 넘기면 JPA가 알아서 쿼리문을 작성하고 JDBC API를 사용해서 DB를 보내고 결과를 받은 다음에 ResultSet 결과를 객체에 다 매핑해줍니다. 그렇기때문에 코드한줄로 조회기능을 애플리케이션 단에서 가능하게 됩니다.

 

 

JPA 역사

해당 역사는 기본편에서 한번 보았던 내용인 것 같아 따로 정리하지않고 링크를 아래 추가했습니다.

 

 

[Spring/기본편] 1. 객체 지향 프로그래밍 및 스프링 개념 정리

목표 스프링을 왜 만드는가? 이유와 핵심원리 스프링 기본 기능 학습 스프링 본질 깊은 이해 객체 지향 설계에 대한 고민을 할 수 있게 해줌 서론 2000년대 초반에는 자바 정파 기술에는 EJB(Enterpr

qazyj.tistory.com

 

 

JPA는 표준 명세

  • JPA는 인터페이스의 모음
  • JPA 2.1 표준 명세를 구현한 3가지 구현체
  • 하이버네이트(대표적), EclipseLink, DataNucleus

 

 

JPA를 왜 사용해야 하는가?

  • SQL 중심적인 개발에서 객체 중심으로 개발
  • 생산성
  • 유지보수
  • 패러다임의 불일치 해결
  • 성능
  • 데이터 접근 추상화와 벤더 독립성
  • 표준

 

생산성 - JPA와 CRUD

  • 저장
    • jpa.persist(member)
  • 조회
    • Member member = jpa.find(memberId)
  • 수정 (fantastic)
    • member.setName("updateName")
  • 삭제
    • jpa.remove(member)

코드가 모두 만들어져있고 가져다 쓰기만해서 기존 CRUD를 하기위해 SQL 문을 작성하던 번거로움이 사라졌습니다.

 

 

유지보수 - 기존 : 필드 변경시 모든 SQL 수정

위에서 느꼈던 기획자에 요청에 의해 필드가 변경될 때 모든 SQL 문을 수정해야하는 번거로움이 있었습니다. 하지만 아래와 같이 JPA에서는 JAVA 애플리케이션 단에서 컬럼만 추가하면 됩니다. 따라서 유지보수 측면에서 훨씬 우월하다고 볼 수 있습니다.

 

JPA와 패러다임의 불일치 해결

  1. JPA와 상속
  2. JPA와 연관관계
  3. JPA와 객체 그래프 탐색
  4. JPA와 비교하기

 

JPA와 상속

이전에 보았던 상속 관계입니다. 

 

JPA와 상속 - 저장

이전에 SQL 문을 작성했던 번거로움이 사라진 것을 볼 수 있습니다. 또한, 상속관계에 있는 album을 보고 알아서 ITEM의 INSERT 쿼리문도 작성해주는 것을 볼 수 있습니다.

 

JPA와 상속 - 조회

기가 막힙니다. JPA에 앨범의 PK 값을 넘기면 JPA에서 ITEM과 ALBUM을 조인해서 데이터를 가져오는 것을 볼 수 있습니다. 패러다임이 맞지 않는 부분을 JPA가 해결해줍니다. 

 

 

JPA와 연관관계, 객체 그래프 탐색

member에 team을 set한 뒤 member를 jpa에 보내 DB에 저장합니다. 후에 조회하여 member로 team을 조회하면 가져올 수 있습니다. 마치 자바 컬렉션에 넣었던 것 처럼!!

 

 

신뢰할 수 있는 엔티티, 계층

JPA를 통해서 member 객체를 가져온거면 객체 그래프를 자유롭게 모두 다 탐색할 수 있습니다. JPA는 지연로딩이라는 기능덕분에 member.getTeam()이나 member.getOrder()를 해서 실제 객체를 사용해서 조회하는 시점에 SQL을 보내 채워주는 기능이 있습니다. 그렇기때문에 자유롭게 탐색할 수 있습니다. 그렇기때문에 신뢰할 수 있는 엔티티가 될 수 있습니다.

 

JPA와 비교하기

동일한 트랙잭션에서 조회한 엔티티는 같음을 보장합니다. 이것이 가능한 이유는 강의를 보다보면 이해할 수 있다고 합니다. (아직 이해 안됨)

 

 

JPA의 성능 최적화 기능

  1. 1차 캐시와 동일성(identity) 보장
  2. 트랜잭션을 지원하는 쓰기 지연 (transactional write-behind)
  3. 지연 로딩 (Lazy Loading)

JPA를 사용하면 성능이 떨어질 수도 있다고 생각할 것 입니다. 이렇게 생각하는 이유는 제 생각으론  기존에는 JAVA 애플리케이션에서 JDBC API로 바로 사용했지만, JPA를 사용하면 중간에 JPA를 거치기때문에 구조가 한단계 더 깊어졌기 때문입니다. 

계층 사이에 중간 계층이 있으면 항상 할 수 있는 것이 있습니다. 바로 모아서 쏘는 버퍼링과 읽을 때 캐싱하는 것을 할 수 있습니다. (마치 CPU와 메모리 사이 캐시기능) JAVA 애플리케이션과 JDBC API의 중간계층으로 JPA가 있기 때문에 SQL을 사용하는 것보다 이러한 부분들을 최적화 할 수 있습니다. 

 

 

1차 캐시와 동일성 보장

  1. 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상
  2. DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read 보장 (DB 지식이 많이 있어야하기 때문에 나중에 이해하고 이해해야할 부분, 현재는 알아두기만 할 것)

아까 조회했던 코드입니다. 첫번째 조회는 SQL을 통해 조회하고 두번째는 캐시를 통해 데이터를 가져옵니다. 그렇기때문에 m1과 m2는 같고 조회 성능을 향상시킬 수 있습니다. 실제로 캐싱기능을 지원해주는 것이 아닌 트랜잭션을 통해 제공해주는 기능이기 때문에 굉장히 짧은 시간에만 사용이 가능해서 실무에서 큰 도움은 되지 않는다고 합니다. 

 

 

트랜잭션을 지원하는 쓰기 지연 - INSERT

  1. 트랜잭션을 커밋할 때까지 INSERT SQL을 모음
  2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송

JDBC BATCH 기능인 한번에 여러 SQL 기능을 사용해서 한번에 SQL 전송을 하는 기능이 있습니다. JPA를 사용하지 않는 경우에는 상당히 복잡하다고 하는데 JPA를 사용하면 위와같이 간단하게 사용할 수 있습니다. 

 

 

트랜잭션을 지원하는 쓰기 지연 - UPDATE

  1. UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
  2. 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋

 

 

지연 로딩과 즉시 로딩

  • 지연 로딩 : 객체가 실제 사용될 때 로딩
  • 즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회

JPA에서 중요한 부분 중 하나입니다. 위 그림과 같이 지연 로딩은 객체가 사용될 때 로딩을 합니다. 지연 로딩의 많은 쿼리가 발생하는 단점을 보완한 기능인데 Member 조회해서 사용할 때 team을 거의 같이 사용한다고 가정될 때 member를 로딩할 때 team도 같이 조회해서 가져오는 것을 말합니다. 옵션을 통해 지연 로딩과 즉시 로딩을 껐다 켤 수 있다고 합니다. 김영한님은 지연 로딩으로 코드를 다 작성한 후 최적화할 때 즉시 로딩이 필요한 부분만 최적화를 진행한다고 합니다.

 

 

 

 

마지막으로

ORM이라는 기술은 객체와 RDB 두 기둥위에 있는 기술입니다. JPA만 잘 안다고해서 잘 할 수 있는 것이 아니고 RDB만 잘 안다고해서 잘할 수 있는 것이 아닙니다. JPA는 객체와 RDB 두 가지 사이에서 밸런스를 잘 맞춰야합니다. 한 가지에 편향하지 않고 JPA를 공부하더라도 RDB도 꾸준하게 같이 공부해야 한다. 꼭! 명심해!

 

 

 

 

 

출처

 

자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의

JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

 

2961번: 도영이가 만든 맛있는 음식

첫째 줄에 재료의 개수 N(1 ≤ N ≤ 10)이 주어진다. 다음 N개 줄에는 그 재료의 신맛과 쓴맛이 공백으로 구분되어 주어진다. 모든 재료를 사용해서 요리를 만들었을 때, 그 요리의 신맛과 쓴맛은

www.acmicpc.net

 

 

 


 

 

 

  • 풀이

 

문제는 신맛, 쓴맛의 재료만 있고 신맛과 쓴맛의 조합은 최대 10가지가 있습니다.

조합을 할때마다 신맛끼리는 곱해지고, 쓴맛끼리는 더해집니다.

적절히 조합하여 신맛과 쓴맛의 절대값차이가 가장 적은 수를 구하면 되는 문제입니다.

 

일단 조합은 최대 10가지로 모든 경우의 수를 확인하는 완탐방식을 생각했습니다. 시간은 1초이고, 10!의 시간으로 완전탐색은 1초안에 될 것이라고 생각했습니다.

 

참고로 이 문제는 비트마스킹으로 조금 더 쉽게 구현할 수 있습니다.

 

비트마스킹의 방법은 1 << N을 함으로써 2^N번을 바깥 for문을 통해서 확인합니다.

0000

0001

0010

0011

0100

0101

....

1111

이런식으로 N개의 수 중 택한 경우는 1, 택하지않은 경우는 0으로 모든 경우의 수를 확인할 수 있게합니다.

안쪽 for문에서는 0~3까지의 수를 반복함으로써 바깥 for문의 1의 자리, 2의 자리, 3의 자리, 4의 자리의 숫자가 1인지 0인지 확인하고, 1이면 신맛, 쓴맛 추가 0이면 작없을 하지 않는 방법으로 진행합니다.

 

 

  • 코드

 

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N;
	static long answer;
	static boolean[] check;
	static ArrayList<Flavor> flavor;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		N = in.nextInt();
		check = new boolean[N];
		answer = Integer.MAX_VALUE;
		flavor = new ArrayList<>();
		for (int i = 0; i < N; i++)
			flavor.add(new Flavor(in.nextInt(), in.nextInt()));
		dfs(0);
	}

	private static void dfs(int index) {
		if (index == N) {
			int sour = 1;
			int bitter = 0;
			int count = 0;
			for (int i = 0; i < check.length; i++) {
				if (!check[i])	continue;
				count++;
				sour *= flavor.get(i).sour;
				bitter += flavor.get(i).bitter;
			}
			if (count == 0)
				return; 
			
			if (answer > Math.abs(sour - bitter))
				answer = Math.abs(sour - bitter);
			return;
		}
		
		check[index] = true;
		dfs(index + 1);
		check[index] = false;
		dfs(index + 1);
	}
}

class Flavor {
	int sour;
	int bitter;

	public Flavor(int sour, int bitter) {
		this.sour = sour;
		this.bitter = bitter;
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

 

  • 비트마스킹 코드

 

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N;
	static long answer;
	static boolean[] check;
	static ArrayList<Flavor> flavor;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		N = in.nextInt();
		check = new boolean[N];
		answer = Integer.MAX_VALUE;
		flavor = new ArrayList<>();
		for (int i = 0; i < N; i++)
			flavor.add(new Flavor(in.nextInt(), in.nextInt()));
		
		for(int i = 1; i < 1 << N; i++) {
			int sour = 1, bitter = 0;
			for(int j = 0; j < N; j++) {
				if((i & 1<<j) > 0) {
					sour *= flavor.get(j).sour;
					bitter += flavor.get(j).bitter;
				}
			}
			answer = Math.min(answer, Math.abs(sour-bitter));
		}
	}
}

class Flavor {
	int sour;
	int bitter;

	public Flavor(int sour, int bitter) {
		this.sour = sour;
		this.bitter = bitter;
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

7569번: 토마토

첫 줄에는 상자의 크기를 나타내는 두 정수 M,N과 쌓아올려지는 상자의 수를 나타내는 H가 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M ≤ 100, 2 ≤ N ≤ 100,

www.acmicpc.net

 

 

 


 

 

 

  • 풀이

 

문제를 간단히 말하자면 3차원의 NxMxH칸에 토마토가 있을수도, 없을수도 있습니다. 또한 토마토는 익은 토마토 or 익지 않은 토마토가 있습니다.

 

1초가 지날때마다 익은 토마토주변 2차원 기준 상하좌우, 3차원 기준 위칸과 아래칸에 익지 않은 토마토를 익게해줍니다.

 

토마토가 모두 익을 때 걸리는 시간 or 토마토를 모두 익히지 못한다면 -1을 출력하면 되는 문제입니다.

 

 

가장 먼저 떠오른 해결책은 너비우선탐색 이였습니다. 익은 토마토의 위치를 queue에 넣어준 뒤, 1초마다 넣어둔 익은 토마토의 위치를 꺼내서 상하좌우, 위아래칸을 탐색하면 된다고 생각했습니다.

 

기존 너비우선탐색 문제들은 2차원에서 탐색하는 문제였는데 해다아 문제는 3차원에서 너비우선탐색을 해야되는 문제입니다. 하지만 어렵지 않습니다. 기존 탐색은 인덱스별로 4번을 한다고한다면 3차원은 위,아래만 추가되기 때문에 탐색을 6번을 하기만하면 되기 때문입니다.

 

이를 기준으로 아래의 코드를 작성하였습니다.

 

만약 4번의 너비우선탐색을 처음 보는 분들은 다른 2차원의 너비우선탐색의 문제들을 풀어본다음에 풀어보시면 좋을 것 같습니다.

 

 

 

  • 코드

 

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N, M, H, answer;
	static int[][][] array;
	static boolean[][][] check;
	static Queue<Node> queue;
	static int[] dx = {-1,1,0,0,0,0};
	static int[] dy = {0,0,-1,1,0,0};
	static int[] dh = {0,0,0,0,-1,1};

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		M = in.nextInt();
		N = in.nextInt();
		H = in.nextInt();
		array = new int[H][N][M];
		check = new boolean[H][N][M];
		queue = new LinkedList<>();
		for(int i = 0 ; i < H; i++) {
			for(int j = 0; j < N; j++) {
				for(int z = 0; z < M; z++) {
					array[i][j][z] = in.nextInt();
					if(array[i][j][z] == 1) {
						queue.add(new Node(j,z,i));
						check[i][j][z] = true;
					}
				}
			}
		}
		bfs();
		for(int i = 0 ; i < H; i++) {
			for(int j = 0; j < N; j++) {
				for(int z = 0; z < M; z++) {
					if(array[i][j][z] == 0) { 
						answer = -1;
						break;
					}
				}
			}
		}
		if(answer != -1) answer--;
	}
	
	public static void bfs() {
		while(!queue.isEmpty()) {
			int size = queue.size();
			
			while(size-->0) {
				Node node = queue.poll();
				for(int direction = 0; direction < 6; direction++) {
					int r = node.x + dx[direction];
					int c = node.y + dy[direction];
					int h = node.h + dh[direction];
					
					if(r < 0 || c < 0 || h < 0 || r >= N || c >= M || h >= H) continue;
					if(array[h][r][c] != 0 || check[h][r][c]) continue;
					
					check[h][r][c] = true;
					array[h][r][c] = 1;
					queue.add(new Node(r,c,h));
				}
			}
			answer++;
		}
	}
}

class Node {
	int x;
	int y;
	int h;
	
	public Node(int x, int y, int h) {
		this.x = x;
		this.y = y;
		this.h = h;
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

 

 

 

7453번: 합이 0인 네 정수

첫째 줄에 배열의 크기 n (1 ≤ n ≤ 4000)이 주어진다. 다음 n개 줄에는 A, B, C, D에 포함되는 정수가 공백으로 구분되어져서 주어진다. 배열에 들어있는 정수의 절댓값은 최대 228이다.

www.acmicpc.net

 

 


 

 

  • 풀이

 

N개의 배열 크기를 갖고있는 4개의 배열의 값을 다 더했을 때 0을 만들 수 있는 개수를 찾는 문제입니다.

 

처음에 시간이 12초로 넉넉하기 때문에 완전탐색을 사용했습니다. 4000*3999*3998*3997의 시간복잡도로 11억 9천..으로 12초 정도 걸릴 것으로 예상하고 풀어봤지만, 시간 초과로 통과되지 않았습니다.

 

두번째론 HashMap을 사용해서 1/2번째 배열을 모두 더한 값, 3/4번째 배열을 모두 더한 값을 HashMap에 넣은 뒤 두 HashMap을 더했을 때 0이되는 수를 찾아서 해결해봤지만 시간 초과로 통과되지 않았습니다. (O(N^2))라고 생각했는데 왜 안되는지... ==> 찾아보니 해시 충돌 시 O(N)이 되서 시간 초과가 난 것 같습니다.

 

세번째로 upperbound, lowerbound로 풀었습니다. 값을 찾을 시 가장 오른쪽의 인덱스와 가장 왼쪽의 인덱스를 통해 찾는 값이 몇개인지 이분 탐색을 통해 찾고 인덱스의 차를 통해 개수를 더해주는 방식입니다. 기존 해시 충돌시 O(N)의 시간복잡도가 나오는 hashmap대신 이분탐색으로 O(log n)으로 풀어보려했습니다. 하지만 해당 방식도 시간 초과로 통과되지 않았습니다.

 

네번째로 투포인터를 사용해서 풀었습니다. AB를 더한 배열은 왼쪽 index부터, CD를 더한 배열은 오른쪽 index부터 안쪽으로 가며 AB[왼쪽index]+CD[오른쪽index]가 0일시 같은 값이 몇개인지 구해주고 곱해서 개수를 더해줍니다. 이때, count를 세는 자료형을 long으로 해주어야합니다. count는 최대 16000000인데, count가 두개이므로 16000000*16000000으로

256000000000000가 되어 int 범위를 초과하기 때문입니다.

 

 

 

  • 성공 코드

 

 

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N;
	static long answer;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		N = in.nextInt();
		answer = 0;
		int[][] array = new int[N][4];
		int[] AB = new int[N * N];
		int[] CD = new int[N * N];

		for (int i = 0; i < N; i++) {
			for (int j = 0; j < 4; j++) {
				array[i][j] = in.nextInt();
			}
		}

		int index = 0;
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < N; j++) {
				AB[index] = array[i][0] + array[j][1];
				CD[index] = array[i][2] + array[j][3];
				index++;
			}
		}

		Arrays.sort(AB);
		Arrays.sort(CD);
		
		twoPoint(AB, CD);
	}

	private static void twoPoint(int[] AB, int[] CD) {
		int left = 0;
		int right = N*N-1;
		
		while(left<N*N && right >-1) {
			int valueOfAB = AB[left];
			int valueOfCD = CD[right];
			int sum = valueOfAB+valueOfCD;
			
			if(sum<0) {
				left++;
			}else if(sum>0){
				right--;
			}else {
				long count1 = 0, count2 = 0;
				while(left<N*N && valueOfAB==AB[left]) {
					left++;
					count1++;
				}
				while(right>-1 &&valueOfCD==CD[right]) {
					right--;
					count2++;
				}
				answer+= count1*count2;
			}
		}
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

 

  • 실패 코드

dfs

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N, answer;
	static int[][] array;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);
		
		N = in.nextInt();
		answer = 0;
		array = new int[N][4];
		
		for(int i = 0; i < N; i++) {
			for(int j = 0; j < 4; j++) {
				array[i][j] = in.nextInt();
			}
		}
		
		dfs(0, 0);
	}
	
	private static void dfs(int index, int sum) {
		if(index == 4) {
			if(sum == 0) answer++;
			return;
		}
		
		for(int i = 0 ; i < N; i++) {
			dfs(index+1, sum+array[i][index]);
		}
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}


HashMap

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N, answer;
	static int[][] array;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);
		
		N = in.nextInt();
		answer = 0;
		array = new int[N][4];
		
		for(int i = 0; i < N; i++) {
			for(int j = 0; j < 4; j++) {
				array[i][j] = in.nextInt();
			}
		}
		
		Map<Integer, Integer> map1 = new HashMap<>();
		Map<Integer, Integer> map2 = new HashMap<>();
		
		for(int i = 0; i < N; i++) {
			for(int j = 0 ; j< N; j++) {
				int value1 = array[i][0]+array[j][1];
				int value2 = array[i][2]+array[j][3];
				
				if(map1.containsKey(value1)) {
					map1.put(value1, map1.get(value1)+1);
				} else {
					map1.put(value1, 1);
				}
				
				if(map2.containsKey(value2)) {
					map2.put(value2, map2.get(value2)+1);
				} else {
					map2.put(value2, 1);
				}
			}
		}
		
		map1.forEach((key, value) ->{
			if(map2.containsKey(key*-1)) {
				answer += value*map2.get(key*-1);
			}
		});
		
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

upperbound / lowerbound

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N, answer;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		N = in.nextInt();
		answer = 0;
		int[][] array = new int[N][4];
		int[] AB = new int[N * N];
		int[] CD = new int[N * N];

		for (int i = 0; i < N; i++) {
			for (int j = 0; j < 4; j++) {
				array[i][j] = in.nextInt();
			}
		}

		int index = 0;
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < N; j++) {
				AB[index] = array[i][0] + array[j][1];
				CD[index] = array[i][2] + array[j][3];
				index++;
			}
		}

		Arrays.sort(CD);
		
		for (int key : AB) {
			int upper = upperBound(CD, key * -1);
			int lower = lowerBound(CD, key * -1);
			answer += (upper - lower);
		}
	}

	private static int upperBound(int[] array, int find) {
		int left = 0;
		int right = array.length;
		while (left <= right) {
			int mid = (left + right) / 2;
			if (array[mid] <= find) {
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		return left;
	}

	private static int lowerBound(int[] array, int find) {
		int left = 0;
		int right = array.length;
		while (left <= right) {
			int mid = (left + right) / 2;
			if (array[mid] < find) {
				left = mid + 1;
			} else {
				right = mid - 1;
			}
		}
		return left;
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

1561번: 놀이 공원

첫째 줄에 N(1 ≤ N ≤ 2,000,000,000)과 M(1 ≤ M ≤ 10,000)이 빈칸을 사이에 두고 주어진다. 둘째 줄에는 각 놀이기구의 운행 시간을 나타내는 M개의 자연수가 순서대로 주어진다. 운행 시간은 1 이상 30

www.acmicpc.net

 

 

 


 

 

 

  • 풀이

 

 

놀이공원에서 M개의 놀이기구를 타기위해 N명의 사람들이 기다리고있다. N명의 사람들은 운행중이지 않은 놀이기구 중 가장 앞 번호의 놀이기구를 탄다고 했을 때, 맨 마지막인 N번째 사람은 M개중 몇번째 놀이기구를 탈 것인가?? 를 묻는 문제입니다.

 

일단, N의 값이 20억이기 때문에 1명, 1명 몇번째 놀이기구를 타는지를 계산하면 적어도 1초에 1억번 계산한다 했을 때 20초가 걸린다고 생각하여 시간 초과가 뜰 것이라고 생각했습니다.

 

시간을 줄이기 위해 다른 방법을 생각해야만 했는데, 사람을 세는 것이 아닌 몇 초 후가 지났을 때 몇명이 탔는가에 대해 초점을 맞추면 이분 탐색이 가능합니다. 이분 탐색은 시간복잡도가 O(log N)으로 매우 빠르기때문에 시간안에 들어올거라 생각했습니다.

 

이분 탐색의 풀이 과정은 아래와 같습니다.

  1. 이분 탐색을 통해 마지막 사람이 타는 시간이 몇초 후인지 알아냅니다.
  2. 이분 탐색을 통해 얻어온 시간 전에 몇명의 사람이 탔는지 구합니다. (예를들어, 10초 후에 N번째  사람이 탄다고 가정하면 9초 후에 몇명의 사람이 탔는지 더해놓습니다.)
  3. 반복문을 통해 N 번째 사람이 타는 시간에 N번째 전 사람들이 타는 횟수를 더해줍니다. 더하다가 N번째 사람이 탑승하면 해당 놀이기구 번호를 저장해주고 반복문을 종료합니다. 

 

참고

right 값을 선언할 때 long으로 타입변환을 시키지 않으면 제대로된 값을 구해오지 못합니다.

N을 int, M을 int로 받아왔을 때 N값이 20억, M값이 1, 놀이기구 시간이 2일 경우, N/M*(놀이기구 중 최대 시간)을 하면 40억으로 int형으로는 다 표현할 수 없기 때문입니다. 

 


 

 

  • 코드

 

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N, M, answer;
	static int[] array;

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		N = in.nextInt();
		M = in.nextInt();
		answer = 0;
		array = new int[M+1];
		int max = 0;
		
		if(N <= M) {
			answer = N;
			return;
		}
		
		
		for(int i = 1; i <= M; i++) {
			array[i] = in.nextInt();
			max = Math.max(max, array[i]);
		}
		
		long high = binarySearch(max);
		
		long sum = M;
		for (int i = 1; i <= M; i++)
			sum += (high - 1) / array[i];

		for (int i = 1; i <= M; i++) {
			if (high % array[i] == 0)
				sum++;

			if (sum == N) {
				answer = i;
				break;
			}
		}
	}
	
	private static long binarySearch(int max) {
		long left = 0;
		long right = (long) N/M*max;
		
		while(left <= right) {
			long mid = (left + right) / 2;
			
			long sum = M;
			for(int i = 1; i <= M; i++) {
				sum += mid/array[i];
			}
			
			if(sum < N)
				left = mid +1;
			else
				right = mid -1;
		}
		
		return left;
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

16930번: 달리기

진영이는 다이어트를 위해 N×M 크기의 체육관을 달리려고 한다. 체육관은 1×1 크기의 칸으로 나누어져 있고, 칸은 빈 칸 또는 벽이다. x행 y열에 있는 칸은 (x, y)로 나타낸다. 매 초마다 진영이는

www.acmicpc.net

 

 


 

 

 

  • 풀이

 

시작점(x1,y1)부터 끝점(x2,y2)까지 최단거리로 이동할 수 있는 값 or 이동할 수 없으면 -1을 출력하면 되는 문제입니다.

 

조건은 아래와 같습니다.

  • 벽은 지나갈 수 없다.
  • 1초에 이동할 수 있는 칸의 최대 개수는 K개 이다.

 

위의 조건을 감안하여 해결해낸 풀이는 아래와 같습니다.

 

  1. 큐를 통해 좌표를 꺼내옵니다.
  2. 상, 하, 좌, 우를 각각 K번씩 이동합니다.
    1. 벽이 있으면 더이상 이동하지 않습니다.
    2. 한 번도 이동한적이 없다면 값을 초기화 해주고 큐에 추가합니다.
    3. 현재 이동할 값이랑 같다면 추가해줄 필요가 없으므로 지나갑니다.
  3. 이동한 값이 (x2,y2)라면 값을 초기화해 준 후 종료합니다.

 

 

 

  • 코드

 

import java.io.IOException;
import java.io.InputStream;
import java.util.*;

public class Main {
	static int N, M, K, x1, x2, y1, y2, answer;
	static boolean[][] wall;
	static int[][] visit;
	static int[] dx = { 0, 0, -1, 1 };
	static int[] dy = { -1, 1, 0, 0 };

	public static void main(String[] args) throws Exception {
		SetData();
		System.out.println(answer);
	}

	private static void SetData() throws Exception {
		InputReader in = new InputReader(System.in);

		N = in.nextInt();
		M = in.nextInt();
		K = in.nextInt();
		wall = new boolean[N + 1][M + 1];
		for (int i = 1; i <= N; i++) {
			String s = in.nextLine();
			for (int j = 1; j <= M; j++) {
				if(s.charAt(j - 1)=='#')
					wall[i][j] = true;
			}
		}

		x1 = in.nextInt();
		y1 = in.nextInt();
		x2 = in.nextInt();
		y2 = in.nextInt();
		answer = Integer.MAX_VALUE;
		visit = new int[N + 1][M + 1];
		for (int i = 1; i <= N; i++) {
			for (int j = 1; j <= M; j++) {
				visit[i][j] = Integer.MAX_VALUE;
			}
		}

		bfs();

		// 이동할 수 없는 경우
		if (answer == Integer.MAX_VALUE)
			answer = -1;
	}

	private static void bfs() {
		Queue<Node> pq = new LinkedList<>();
		pq.add(new Node(x1, y1, 0));
		visit[x1][y1] = 0;

		while (!pq.isEmpty()) {
			Node node = pq.poll();
			
			if (node.x == x2 && node.y == y2) {
				answer = Math.min(answer, node.distance);
				break;
			}

			for (int direction = 0; direction < 4; direction++) {
				for (int k = 1; k <= K; k++) {
					int r = node.x + dx[direction]*k;
					int c = node.y + dy[direction]*k;

					if(r < 1 || c < 1 || r > N || c > M || wall[r][c]) break;

					if (visit[r][c] == Integer.MAX_VALUE) {
						visit[r][c] = node.distance + 1;
						pq.add(new Node(r,c,node.distance + 1));
					}
					else if (visit[r][c] == node.distance + 1) {
						continue;
					}
					else break;
				}
			}
		}
	}
}

class Node{
	int x;
	int y;
	int distance;

	public Node(int x, int y, int distance) {
		this.x = x;
		this.y = y;
		this.distance = distance;
	}
}

class InputReader {
	private final InputStream stream;
	private final byte[] buf = new byte[8192];
	private int curChar, snumChars;

	public InputReader(InputStream st) {
		this.stream = st;
	}

	public int read() {
		if (snumChars == -1)
			throw new InputMismatchException();
		if (curChar >= snumChars) {
			curChar = 0;
			try {
				snumChars = stream.read(buf);
			} catch (IOException e) {
				throw new InputMismatchException();
			}
			if (snumChars <= 0)
				return -1;
		}
		return buf[curChar++];
	}

	public int nextInt() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		int res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public long nextLong() {
		int c = read();
		while (isSpaceChar(c)) {
			c = read();
		}
		int sgn = 1;
		if (c == '-') {
			sgn = -1;
			c = read();
		}
		long res = 0;
		do {
			res *= 10;
			res += c - '0';
			c = read();
		} while (!isSpaceChar(c));
		return res * sgn;
	}

	public int[] nextIntArray(int n) {
		int a[] = new int[n];
		for (int i = 0; i < n; i++) {
			a[i] = nextInt();
		}
		return a;
	}

	public String nextLine() {
		int c = read();
		while (isSpaceChar(c))
			c = read();
		StringBuilder res = new StringBuilder();
		do {
			res.appendCodePoint(c);
			c = read();
		} while (!isEndOfLine(c));
		return res.toString();
	}

	public boolean isSpaceChar(int c) {
		return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == -1;
	}

	private boolean isEndOfLine(int c) {
		return c == '\n' || c == '\r' || c == -1;
	}
}

 

 

캐시 기본 동작

 

캐시가 없을 때

첫 번째 요청

두 번째 요청

위와 같이 똑같은 작업을 합니다.

 

캐시가 없을 때

  • 데이터가 변경되지 않아도 계속 네트워크를 통해서 데이터를 다운로드 받아야 합니다.
  • 인터넷 네트워크는 매우 느리고 비쌉니다.
  • 브라우저 로딜 속도가 느립니다.
  • 때문에 느린 사용자 경험

 

 

캐시 적용

첫 번째 요청

cache-control을 통해 캐시가 유효한 시간(초)를 설정할 수 있습니다. 위의 경우 60초동안은 캐시가 유효하게 됩니다.

두 번째 요청

 

캐시 적용

  • 캐시 덕분에 캐시 가능 시간동안 네트워크를 사용하지 않아도 됩니다.
  • 비싼 네트워크 사용량을 줄일 수 있습니다.
  • 브라우저 로딩 속도가 매우 빠릅니다.
  • 때문에 빠른 사용자 경험

 

세 번째 요청 - 캐시 시간 초과

 

캐시 시간 초과

  • 캐시 유효 시간이 초과하면, 서버를 통해 데이터를 다시 조회하고, 캐시를 갱신해야 합니다.
  • 이때 다시 네트워크 다운로드가 발생합니다.

 

 

 

검증 헤더와 조건부 요청1

캐시 유효 시간이 초과해서 서버에 다시 요청하면 알의 두 가지 상황이 나타납니다.

  1. 서버에서 기존 데이터를 변겸함
  2. 서버에서 기존 데이터를 변경하지 않음

1번의 경우는 다시 받아야 하는 것이지만, 2번의 경우는 다시 받을 필요가 없는데 다시 다운받는 불필요한 다운로드를 하게됩니다.

이러한 문제를 해결하기 위한 것이 검증 헤더와 조건부 요청입니다.

검증 헤더는 Last-Modified라는 마지막으로 수정된 시간을 통해 검증을 하는 헤더입니다.

조건부 요청은 if-modified-since라는 수정된 시간을 통해 캐시를 다시 사용해도 되는지에 대한 요청입니다.

 

검증 헤더 추가

첫 번째 요청

Last-Modified는 마지막에 수정된 시간입니다. (원래는 UTC 표기법을 사용해야 합니다.)

기존 캐시엔 최종 수정일을 등록하지 않았지만, 서버 응답에서 Last-Modified로 왔기때문에 캐시에 추가해줍니다.

캐시 시간 초과된 후 두 번째 요청

1                                                                                                                            2
3                                                                                                                            4
5                                                                                                                            6

  1. 캐시에 데이터 최종 수정일이 적혀있으면, HTTP 요청 헤더에 if-modified-since에 날짜를 붙인다음 서버로 요청합니다.
  2. 서버에서 요청을 받을 때, if-modified-since가 왔음을 확인합니다.
  3. 서버에서 if-modifiefd-since와 같은 데이터 수정일을 확인합니다.
  4. 같은 수정일이 있는 데이터가 있으면, HTTP 응답을 만들 때 HTTP Body를 비우고 304 Not Modified로 보냅니다. 나머진 이전에 보냈던 헤더와 똑같이 보냅니다. (이렇게하면 0.1M 전송하는 데이터 용량을 줄일 수 있습니다.)
  5. 웹 브라우저에서는 304 Not Modified를 보면 캐시에 있는데이터와 서버에 있는 데이터가 동일하니 캐시에 있는 데이터를 사용해도 됨을 알 수 있습니다. 이를받고 응답 결과를 재사용하며, 헤더 데이터를 갱신합니다.
  6. 해당 캐시는 다시 60초동안 유효하게됩니다.

 

검증 헤더와 조건부 요청 - 정리

  • 캐시 유효 시간이 초과해도, 서버의 데이터가 갱신되지 않으면
  • 304 Not Modified + 헤더 메타 정보만 응답(body X)
  • 클라이언트는 서버가 보낸 응답 헤더 정보로 캐시의 메타 정보를 갱신
  • 클라이언트는 캐시에 저장되어 있는 데이터 재활용
  • 결과적으로 네트워크 다운로드가 발생하지만 용량이 적은 헤더 정보만 다운로드
  • 매우 실용적인 해결책

실제로 개발자 도구를 열었을 때 Status의 색이 연한 것은 캐시에서 가져온 데이터입니다.

캐시에 저장된 데이터를 더블클릭 -> 개발자 도구 -> 새로고침 을 하면 304 status로 나온다고하는데 저는 잘 안되네요ㅠㅠ

 

정리

  • If-Modified-Since : 이후에 데이터가 수정 되었으면?
  • 데이터 미변경 예시
    • 캐시 : 2020년 11월 10일 10:00:00 vs 서버 : 2020년 11월 10일 10:00:00
    • 304 Not Modified, 헤더 데이터만 전송
    • 전송 용량 0.1M
  • 데이터 변경 예시
    • 캐시 : 2020년 11월 10일 10:00:00 vs 서버 2020년 11월 10일 11:00:00
    • 200 OK, 모든 데이터 전송
    • 전송 용량 1.1M

 

검증 헤더와 조건부 요청 2

  • 검증 헤더
    • 캐시 데이터와 서버 데이터가 같은지 검증하는 데이터
    • Last-Modifeid (위에서 알아보았던 것), ETag (알아 볼 것)
  • 조건부 요청 헤더
    • 검증 헤더로 조건에 따른 분기
    • If-Modified-Since : Last-Modified 사용
    • If-None-Match : ETag 사용
    • 조건이 만족하면 200 OK
    • 조건이 만족하지 않으면 304 Not Modified

 

검증 헤더와 조건부 요청

Last-Modified, If-Modified-Since 단점
  • 1초 미만(0.x초) 단위로 캐시 조정이 불가능 (최대 할 수 있는 단위가 1초이상 단위입니다.)
  • 날짜 기반의 로직 사용
  • 데이터를 수정해서 날짜가 다르지만, 같은 데이터를 수정해서 데이터 결과가 똑같은 경우 (A -> B -> A로 다시 수정된 경우, 데이터는 같지만 마지막 수정일은 다르게 됩니다.)
  • 서버에서 별도의 캐시 로직을 관리하고 싶은 경우
    • 예) 스페이스나 주석처럼 크게 영향이 없는 변경에서 캐시를 유지하고 싶은 경우

 

검증 헤더와 조건부 요청

ETag, If-None-Match (Last-Modified, If-Modified-Since 단점 보완)
  • ETag(Entity Tag)
  • 캐시용 데이터에 임의의 고유한 버전 이름을 달아둠
    • 예) ETag: "v1.0", ETag : "a2jiodwjekjl3"
  • 데이터가 변경되면 이 이름을 바꾸어서 변경함 (Hash를 다시 생성)
    • 예) ETag : "aaaaa" -> ETag : "bbbbb"
  • 진짜 단순하게 ETag만 보내서 같으면 유지, 다르면 다시 받기!

 

ETag, If-None-Match 첫 번째 요청

 

ETag, If-None-Match 두 번째 요청
- 캐시 시간 초과

1                                                                                                                            2

 

3                                                                                                                            4

 

5                                                                                                                            6

 

정리

  • 단순하게 ETag만 서버에 보내서 같으면 유지, 다르면 다시 받기
  • 캐시 제어 로직을 서버에서 완전히 관리
  • 클라이언트는 단순히 이 값을 서버에 제공(클라이언트는 캐시 메커니즘을 모름)
  • 예)
    • 서버는 배타 오픈 기간인 3일 동안 파일이 변경되어도 ETag를 동일하게 유지
    • 애플리케이션 배포 주기에 맞추어 ETag 모두 갱신

 

 

 

캐시와 조건부 요청 헤더

 

캐시 제어 헤더

  • Cache-Control : 캐시 제어
  • Pragma : 캐시 제어 (하위 호환)
  • Expires : 캐시 유효 기간 (하위 호환)

 

Cache-Control

캐시 지시어 (directives)
  • Cache-Control : max-age
    • 캐시 유효 시간, 초 단위
  • Cache-Control : no-cache
    • 데이터는 캐시해도 되지만, 항상 원(origin) 서버에 검증하고 사용 (원 서버란 캐시 서버, 프록시 서버가 아닌 진짜 서버)
  • Cache-Control : no-store
    • 데이터에 민감한 정보가 있으므로 저장하면 안됨 (메모리에서 사용하고 최대한 빨리 삭제)

 

Pragma

캐시 제어 (하위 호환)
  • Pragma : no-cache
  • HTTP 1.0 하위호환

 

Expires

캐시 만료일 지정 (하위 호환)
  • expires : Mon, 01 Jan 1990 00:00:00 GMT
  • 캐시 만료일을 정확한 날짜로 지정
  • HTTP 1.0 부터 사용
  • 지금은 더 유연한 Cache-Control:max-age 권장
  • Cache-Control:max-age와 함께 사용하면 Expires는 무시

 

 

 

프록시 캐시

 

원 서버 직접 접근

origin 서버

이렇게 클라이언트마다 원 서버로 접근하게되면 500ms를 다운로드 받을 때마다 0.5초를 소모하게 됩니다. 이러한 시간을 단축시키고자 프록시 서버라는 것이 등장하게 됩니다.

 

프록시 캐시 도입

첫 번째 요청

이러한 프록시 캐시 서버 덕분에 유튜브, 넷플릭스 같은 서비스를 모든 나라에서 빨리 볼 수 있는 것 입니다. (이를 증명하는 것은 한국서버에서 한국인이 자주 보는 동영상은 로딩 속도가 되게 빠르지만, 한국인이 잘 보지 않는 외국 기술 동영상들을 보면 로딩 속도가 매우 느립니다.)

 

Cache-Control

캐시 지시어(directives) - 기타
  • Cache-Control : public
    • 응답이 public 캐시에 저장되어도 됨
  • Cache-Control : private
    • 응답이 해당 사용자만을 위한 것임, private 캐시에 저장해야 함(기본값) (공용으로 사용되는 이미지는 public에 저장 되도 되지만, 개인 정보같은 경우는 public에 저장되면 안되기 때문)
  • Cache-Control : s-maxage
    • 프록시 캐시에만 적용되는 max-age
  • Age : 60 (HTTP 헤더)
    • 오리진 서버에서 응답 후 프록시 캐시내에 머문 시간(초)

 

 

 

캐시 무효화

 

Cache-Control

확실한 캐시 무효화 응답
  • Cache-Control을 적지 않아도 많이 사용 되는 경우 임의로 캐시에 등록 될 수 있기 때문에, 절대 캐시가 되면 안되는 페이지는 아래와 같은 작업을 해주어야 합니다.
  • Cache-Control : no-cache, no-store, must-revalidate
  • Pragma : no-cahce
    • HTTP 1.0 하위 호환

 

Cache-Control

캐시 지시어(directives) - 확실한 캐시 무효화
  • Cache-Control : no-cache
    • 데이터는 캐시해도 되지만, 항상 원 서버에 검증하고 사용
  • Cache-Control : no-store
    • 데이터에 민감한 정보가 있으므로 저장하면 안됨
  • Cache-Control : must-revalidate
    • 캐시 만료후 최초 조회 시 원 서버에 검증해야 함
    • 원 서버 접근 실패시 반드시 오류가 발생해야 함 - 504 Gateway Timeout
    • must-revalidate는 캐시 유효 시간이라면 캐시를 사용함
  • Pragma : no-cahce
    • HTTP 1.0 하위 호환

 

no-cache vs must-revalidate

no-cache 기본 동작

1

 

2

 

3

3번의 경우 프록시 캐시에서 원 서버의 장애로 장애를 웹 브라우저에 보내기보다는 이전 데이터라도 쓰라며 200 OK를 보내는 경우가 있다고 합니다. 이것이 no-cache의 정책입니다.

 

must-revalidate

위의 no-cache인 캐시 지시어는 프록시 캐시에서 원 서버로 접근하는 네트워크가 단절되도 웹 브라우저한테는 이전의 데이터로라도 사용하라며 200 OK를 보내주지만, must-revalidate의 경우는 원 서버와 네트워크가 되지 않으면 웹 브라우저에게 504 Gateway Timeout의 응답을 줍니다. 이것이 no-cache와 must-revalidate의 차이 입니다.

 

 

 

 

 

출처

 

모든 개발자를 위한 HTTP 웹 기본 지식 - 인프런 | 강의

실무에 꼭 필요한 HTTP 핵심 기능과 올바른 HTTP API 설계 방법을 학습합니다., - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts