TIL - 0817

Stream API

groupingBy

  • Stream의 원소()들을 특정 기준에 따라 그룹핑하는 함수 : 리턴 타입 - Collector<T, ?, Map<K, List>>
    • groupingBy는 인자로 Function 타입(함수형 인터페이스)을 받음 : T타입으로 받아서 R타입으로 리턴(R apply(T t))
public static void main(String[] args) {
    List<Item> items = Arrays.asList(
        new Item(SHOES, "nike shoes"),
        new Item(SHOES, "adidas shoes"),
        new Item(SHOES, "nike shoes2"),
        new Item(FOOD, "떡볶이"),
        new Item(FOOD, "연어"),
        new Item(CLOTHES, "hazzys"),
        new Item(CLOTHES, "polo")
    );

    Map<String, List<Item>> groupItem = items.stream()
                                             .collect(groupingBy(Item::getItemKind));

    groupItem.keySet()
             .stream()
             .map(itemKind -> groupItem.get(itemKind).stream().map(Item::getName).collect(joining(", ")))
             .forEach(System.out::println);
}

partitioningBy

  • groupingBy와 똑같이 그룹을 나누는데, 기준의 타입은 Boolean임
Map<Boolean, List<Item>> groupItem2 = items.stream().collect(partitioningBy(Item::isDiscount));

groupItem2.keySet()
	      	.stream()
			.map(isDiscount -> {
				String name = groupItem2.get(isDiscount).stream().map(Item::getName).collect(joining(", "));
				return String.format("%s: %s", isDiscount ? "세일품목" : "정상품목", name);
			})
			.forEach(System.out::println);
			
			
class Item {
    private PricePolicy pricePolicy;
    private ItemKind itemKind;
    private String name;
    private int price;

    Item(ItemKind itemKind, String name, int price) {
        this.itemKind = itemKind;
        this.name = name;
        this.price = price;
        pricePolicy = RegularPrice.of();
    }

    public Item changePricePolicy(PricePolicy pricePolicy) {
        this.pricePolicy = pricePolicy;
        return this;
    }

    public int getPrice() {
        return pricePolicy.calc(price);
    }

    String getItemKind() {
        return itemKind.name();
    }

    String getName() {
        return name;
    }

    boolean isDiscount() {
        return pricePolicy.isDiscount();
    }
}

enum ItemKind {
    SHOES,
    FOOD,
    CLOTHES
}

interface PricePolicy {

    int calc(int price);

    boolean isDiscount();
}


class RegularPrice implements PricePolicy {
    private static final RegularPrice POLICY = new RegularPrice();

    private RegularPrice() {
    }

    public static PricePolicy of() {
        return POLICY;
    }

    @Override
    public int calc(int price) {
        return price;
    }

    @Override
    public boolean isDiscount() {
        return false;
    }
}

class HalfPrice implements PricePolicy {
    private static final HalfPrice POLICY = new HalfPrice();

    private HalfPrice() {
    }

    public static PricePolicy of() {
        return POLICY;
    }

    @Override
    public int calc(int price) {
        return price / 2;
    }

    @Override
    public boolean isDiscount() {
        return true;
    }
}

HTTP

Connection 4가지 필요한 정보

  1. 클라이언트 IP
  2. 클라이언트 포트번호(응용프로그램 식별)
  3. 서버 IP
  4. 서버 포트번호

HTTP Connection 과정

  1. 웹브라우저는 URL을 DNS 서버에 요청하여 IP를 얻음, 포트는 기본적으로 80포트(지정가능, 스킴://도메인:포트)
  2. TCP 커넥션 맺기 : 서버 소켓은 연결 대기 중, 클라이언트 소켓은 생성 - handshake 과정
  3. 요청 보내기 : 데이터를 쪼개어 패킷에 담아 보냄
  4. 응답 받기 : handshake 과정에서 클라이언트 IP, 포트 정보를 알게됨 - 식별 가능(같은 IP더라도 포트번호로 식별)
  5. 커넥션 끊기
  • 대부분이 소켓 API를 활용함 : 소켓 생성, 데이터 스트림 읽기/쓰기 등

HTTP Connection 성능 향상 시키기와 주의할 점

  • 병렬 커넥션 : 1개의 커넥션이 아닌 한꺼번에 여러 커넥션을 만들고 요청해서 응답받은 뒤 끊어버리는 방식
    • 직렬 커넥션은 순차적으로 처리 - 만약 1개의 html과 3개의 이미지를 응답 받아야한다면, html 응답받고 - 1개씩 이미지를 받아야함
    • 여러개의 커넥션을 한꺼번에 생성해서 일괄적으로 서버에 요청하는 방식 : 멀티쓰레드 처리와 비슷
    • 오히려 더 느릴 때도 있음 : 동시에 내려받고 처리하는 것처럼 보이기때문에 사용자에게는 더 빠르게 보일 수 있음, 또한 많은 메모리 소모
    • 트랜잭션 내에서 많은 커넥션을 허용할 경우 부하가 커짐 - 보통 적은 수(최신 브라우저는 6~8개)의 병렬 커넥션만을 사(허)용
  • 지속 커넥션 : 처리 완료 이후 커넥션을 끊지않고 재사용하는 것을 말함 - TCP 커넥션은 시간이 지날수록 자체 튜닝되어서 더 빨라지므로 유지하는 것이 좋음(TCP 느린 시작, 물론 커넥션 유지될 수록 성능상 부하가 걸리는 것이 문제)
    • 병렬과 지속 : 병렬은 한 트랜잭션이 끝나면 커넥션을 끊고 제거해버리지만 지속은 커넥션을 유지하고 재사용하는 것이 특징
    • 커넥션을 계속 유지하고 있다는 것은 클라이언트 - 서버 모두 자원을 사용해가며 유지하고 있는 것으로 쓸데 없이 유지하다가 오히려 비효율만 낳게 됨
    • 커넥션 생성 방법 : keep-alive(HTTP 1.0+, 기본으로 사용되지는 않음) - Connection과 Keep-Alive(클라이언트, 서버) 헤더로 설정 / 지속 커넥션(HTTP 1.1, 기본 활성화 - 끊기위해서 Connection: close 명시해야 바로 끊김)
    • 주의할 점 : 모든 메세지에 Content-Length가 있어야 기존 메세지와 새 메세지 구분이 가능함

Spring-data-jpa

  • JPA : 객체와 Relation(table)을 맵핑하여 객체를 영속화(영구히 저장) 시켜줌
  • Hibernate : JPA는 표준이고, Hibernate는 표준의 구현체
  • Spring-data-jpa : spring에서 jpa를 사용할 때 사용하기 편하게 도와주는 모듈(data 하위 모듈)

spring에서 jpa

  • 직접적으로 JPA와 Hibernate API를 사용하지않음
/* 이렇게 안함 */
@PersistContext
EntityManager entityManager;

public void saveAccount(Account account) {
    entityManager.persist(account);
    
    // 혹은 - 위는 JPA, 아래는 JPA 구현체 중 하나인 Session(JPA 표준 메소드가 아닌 구현체 메소드로)
    
    Session session = entityManager.unwrap(Session.class);
    session.save(account);
}

/* 이렇게 하지 */
public interface AccountRepository extends CrudRepository {
}

@Autowired
private AccountRepository accountRepository;
accountRepository.save(Account);

사용 설정하기

  • DB 커넥션풀에 담길 커넥션 설정을 위해서 몇가지 설정해줘야함 : 주로 application.properties
    • 여기 정보가 없으면 어느 데이터베이스에 접근해야할지 정보가 없는 것
spring.datasource.url=jdbc:mysql://localhost:3306/exam
spring.datasource.username=example
spring.datasource.password=pass
  • hibernate 설정 사항 자세히 몰랐던 부분 : ddl-auto 설정
    • create : 어플리케이션 구동 시마다 생성해줌(dev option)
    • update : 맵핑되지않는다면(추가되면 - 필드) 스키마 변경을 업데이트해줌(이미 만들어져있는 것에 대해서는 업데이트 안됨)
    • validate : 디비 테이블과 객체 데이터 맵핑이 잘되는지 검증만 함(prod에 적합한 옵션) - update와 달리 업데이트 해주지않음, 맵핑되지않는다면 종료시켜버림
    • 필드가 삭제되어서 맵핑되지않을 때에는 둘 다 에러가 남 : 원래 있던 데이터들을 모두 변경해줘야해서 그런 듯
spring.jpa.hibernate.ddl-auto=create 

사용하기

value 타입 맵핑

  • value 타입이란 별도의 엔티티(테이블)로 만들지는 않고, 엔티티에 속해지는 종속적인 값들
    • primitive type과 ref type이 있음 : primitive는 그대로 사용하면 됨
    • ref type은 어떻게 하나? - 테이블에 저장할 때 혹은 맵핑할 때
    • 이렇게 하면 좋은 점 : 객체 분리하면서 테이블은 똑같이 만들 수 있음
    • 한 타입을 두개 할 경우 테이블 컬럼 이름 구분을 하기위해 : @AttibuteOverrides와 @AttributeOveride를 사용함, 구분해주지 않고 실행하니 에러남
@Embeddable
public class Address {

    private String city;
    private String state;
    private String zipCode;
}

@Entity
public class Account {

    @Embedded
    private Address address;
}

// 결과
 id |      created_date       | city | state | street | zip_code | name  | password | user_id
----+-------------------------+------+-------+--------+----------+-------+----------+----------
  1 | 2018-08-17 15:52:23.208 |      |       |        |          | colin | 1234     | jinhyung
  

/* 테이블 컬럼명 각각 설정해주기 */  
@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "street", column = @Column(name = "home_street")),
    @AttributeOverride(name = "city", column = @Column(name = "home_city")),
    @AttributeOverride(name = "state", column = @Column(name ="home_state")),
    @AttributeOverride(name ="zipCode", column = @Column(name ="home_zipCode"))
})
private Address homeAddress;

@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "street", column = @Column(name = "delivery_street")),
    @AttributeOverride(name = "city", column = @Column(name = "delivery_city")),
    @AttributeOverride(name = "state", column = @Column(name ="delivery_state")),
    @AttributeOverride(name ="zipCode", column = @Column(name ="delivery_zipCode"))
})
private Address deliveryAddress;

관계 맵핑하기 - 객체, Relation 차이가 가장 크고 둘 다 잘 알아야 함

  • 테이블 간의 JOIN과 , 객체 참조변수를 가지는 차이에 대한 맵핑
  • 테이블의 키는 어디에 저장되더라도 조회가 가능한 반면, 객체는 참조 변수를 가져야 양방향 조회(2개의 단방향으로 양방향처럼 만듦)가 가능하다는 점을 알아야함(굳이 객체 상에서 양방향 조회할 필요 없을 때에는 만들어주지않아도 됨, 그런 차이가 있다는 것만)
  • fk를 어디에서 관리할지는 테이블 관점으로 봐야함 : 중복 데이터가 생기지않도록, 데이터 원자성을 지키도록(하나의 row에는 더이상 쪼갤 수 없는 데이터가 저장되어야함 - 여러개가 저장되면 안됨 예를 들어 콤마로 구분된 데이터)
  • 어떤 관계냐에 따라 기본적으로 어느 테이블에서 fk를 관리하도록 하는 쿼리가 자동 생성됨(효율적인 관리) : 1:N일 때 N쪽에서 관리함, fk 이름은 설정해주지않으면 필드명_id임
  • fk를 관리하는 쪽에서만 관계 설정이 가능함 : fk를 관리하고 있으니깐… row 추가할 때마다 fk를 넣지.. fk를 관리하고 있지도 않은 쪽에서 테이블 상 어떻게 관계를 맺어줌… -> 이걸 주인이라고 표현함
  • 1:N일 때 1쪽에서 키를 관리하면 기본적으로 테이블을 새로 생성함 : 아까 적었던 데이터 원자성을 지키기위해서, 중복 저장을 막기위해서
  • M:N일 때에는 테이블을 하나 새로 생성해서 fk를 관리하도록 하는 것이 데이터 중복 저장, 써치에도 유리
  • 2개의 단방향으로(객체 상) 양방향으로 만들 때 fk를 가지지않은 쪽에서는 mappedBy 설정을 해줘야 따로 또 관계를 만들지않음 : 예를 들어 1:N일 때 1쪽에 mappedBy 설정을 해주지않으면 따로 테이블을 만들어버림(테스트 결과 진짜그럼 - 모르면 엄청난 비효율)
@Entity
public class Study extends AbstractEntity {

    private String name;

    @ManyToOne
    @JoinColumn(name = "fk_study_account")
    private Account owner;

    public Study(String name) {
        this.name = name;
    }

    public Study setOwner(Account account) {
        if (!Objects.isNull(owner)) {
            owner = account;
        }
        return this;
    }
}

// 결과 
 id |      created_date       |    name    | fk_study_account
----+-------------------------+------------+------------------
  2 | 2018-08-17 16:44:24.655 | hello java |                1
  
  
// 객체 상에서 양방향 만든다면! - 메소드 호출을 까먹을 수도 있으니 fk를 가지는 쪽에서 관계 맵핑할 때 메소드 호출까지 하도록, 삭제할 떄도 양 단방향 모두 변경해줘야(테이블과 객체의 차이)
@OneToMany(mappedBy = "owner")
private Set<Study> myOwnerStudies = new HashSet<>();

@Transactional
public Study setOwner(Account account) {
    if (Objects.isNull(owner)) {
        owner = account;
        account.addMyOwnerStudy(this);
    }
    return this;
}

@Transactional
public Study changeOwner(Account account) {
    owner.removeStudy(this);
    setOwner(account);
    return this;
}

// mappedBy 설정이 없다면 Join 테이블을 또 만들어버림
Hibernate: 
    insert 
    into
        account_my_owner_studies
        (account_id, my_owner_studies_id) 
    values
        (?, ?)