TIL - 0816

Spring-data-JPA

ORM을 쓰면

  • 도메인 모델 개발
    • 쿼리를 직접 생성하지않음(물론 아예 하지않는 것은 아님 - 필요에 의해 제공 기능으로 쿼리 생성까지 할 수 있음) : SQL 방언까지 신경쓰지않아도 됨(java-ims에서 prod/dev 환경에 따라 다른 RDBMS를 사용했지만 손쉽게 전환이 가능했던 이유)
    • UserDao 혹은 UserRepository와 같이 데이터베이스 접근하는 역할을 만들어줌 : Connection 자원 할당, 해제 등 관리를 개발자가 하지않음
/* 도메인 모델 없이 개발하기 */
String url = "jdbc:postgresql://localhost:5432/springdata";
String username = "jinbro";
String password = "pass";

try (Connection connection = DriverManager.getConnection(url, username, password)) {
    System.out.println("Connections created : " + connection);
    String sql = "CREATE TABLE ACCOUNT (id int, username varchar(255), password varchar(255), primary key (id))";
    try (PreparedStatement statement = connection.prepareStatement(sql)) {
        statement.execute();
    }
} catch (SQLException e) {
    System.out.println(e.getMessage());
}


/* 도메인 모델 개발 */
Account account = new Account("jinbro", 27);
accountRepository.save(account);
  • 중간 계층이 생김
    • 캐시 할 수 있게 됨(같은 트랜잭션) : 실제로 쿼리를 여러번 보내는게 아니라 객체를 저장해뒀다가 해당 객체가 변경되었을 경우 감지하고 변경까지 적용해둠, 최종적으로 최초 상태와 다를 때 쿼리를 날림, 변경이 없고 써치만 했을 경우 최초 1회 쿼리만 날리고 이후부터는 똑같은 객체만 리턴해주면 됨(데이터베이스 커넥션 수를 줄임)
    • lazy loading 가능 : 객체의 필드로 다른 객체가 있을 수 있음 - 해당 부분도 쿼리를 날려 찾아와야하는데 처음부터 찾아오는게 아니라 실제로 사용할 때 찾아오도록 lazy 설정이 가능함, lazy 설정을 했을 때 발생할 수 있는 문제가 있음(N+1 쿼리 문제 - 1개마다 N번의 순회를 하는 문제, 해당 문제가 왜 발생하는지 어떻게 해결하는지 차차 찾아보기)
  • 패러다임 불일치 해결 : OO와 Relational의 패러다임 차이
  • 꼭 알아야하는 것 : JPA만 알아서 개발할 수 있는게 아님 - RDBMS와 쿼리에 대해 알지 못하면 JPA를 제대로 사용할 수 없음, JPA를 어렴풋이 알면 오히려 성능을 망칠 수도 있으니 제대로된 공부가 필요하다…!

OO와 R의 패러다임 차이점과 맵핑

  • 차이
    • 식별의 차이 : 주 키(pk)로 식별과 equals() + hashCode() 식별
    • 데이터 타입의 차이 : VARCHAR, INT 등 정해진 타입과 다양한 객체 타입(클래스 - 기본 제공, 개발자 정의, enum)
    • 상속관계와 연관관계 등 관계 표현 : 테이블 생성 및 JOIN과 extends, implements, 의존(필드)
  • 차이 극복을 위해 ORM 사용 전에는 개발자가 직접 작업을 해줘야했으나 ORM이 패러다임 차이 극복을 위한 작업을 함
    • 생산성 향상
    • 비즈니스 로직 개발에 더 집중할 수 있게 됨

OOP

객체 추상화하기 - 도메인 분석부터

  • 본인이 가진 배경지식을 가지고 추상화를 진행하면 안된다
    • 예를 들어 커피전문점이라는 도메인을 설계할 때 바리스타와 손님 객체가 필요하다, 클래스로 추상화해야하는데 이때 실세계 배경지식으로 사람이라는 클래스를 추상화해버리면 코드 복잡도만 증가할 뿐 문제 해결에 전혀 도움되지않는다. 스태프와 손님으로 추상화한 뒤 스태프는 점장과 점원 더 나아가 바리스타와 계산원으로 세분화를 시키는 것이 더 낫다.
  • 생각의 결과 : 도메인 분석부터 하고 필요한 객체를 먼저 도출한다 -> 객체를 그대로 추상화하면 됨
  • 경험의 결과 : 도메인 분석 후 필요한 객체를 먼저 만들고 시작하기보다 해당 도메인에 대한 경험이 부족하다면 기능을 먼저 생각하고 기능을 만들기위해 테스트 케이스를 만든 뒤 기능을 만들면서 객체를 나누는 것이 좋았음

JAVA

String, StringBuilder, StringBuffer

  • String은 불변 객체 : 값이 바뀌지않음, 대신 바뀐 값을 가지는 객체를 생성할 뿐
    • 잦은 값 변경이 있을 경우 Builder나 Buffer를 고려해야함 : String 객체가 불필요하게 메모리에 쌓이게 되는 것을 방지(메소드 내에서(스택 메모리 내에서) 선언되는 로컬 변수라면 크게 상관없지만) - String 객체에 += 연산을 하면 엄청 느림(매번 새로운 객체)
    • 같은 값을 가지는 String 객체라면 같은 객체 : 두 참조 변수가 같은 객체 주소값을 가짐
String str = new String(new char[]{'a', 'b', 'c'});
String str2 = "abc";

System.out.println(str.hashCode() + ", " + str2.hashCode()); // 같은 값
  • StringBuilder와 StringBuffer의 차이 : 멀티쓰레드 환경에서 문자열에 대한 처리를 하는 객체를 만들었다 그것도 전역변수(공유 변수)로… 그렇다면 Buffer를 사용하는 것이 Thread-Safe함 - Buffer의 메소드(변경, 조회 메소드 - 모든 메소드)들은 동기화 처리가 되어있음(Lock - 하나의 쓰레드가 점유하면 다른 쓰레드는 대기 해야함)

  • 문자열끼리 ‘+’연산자를 통해서 concat 할 경우 1.5부터는 컴파일 단계에서 StringBuilder로 컴파일 됨

    • javap로 역어셈블 해보면 append로 컴파일 된 것을 볼 수 있었음
/* 컴파일 대상 */
public static String concat(String str1, String str2) {
    return str1 + str2;
}


/* 역어셈블 결과 */
public static java.lang.String concat(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: new           #6                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        11: aload_1
        12: invokevirtual #11                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        18: areturn
      LineNumberTable:
        line 16: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  str1   Ljava/lang/String;
            0      19     1  str2   Ljava/lang/String;
}

Stream API

  • Stream API는 최종연산을 하고 난 뒤에는 재사용할 수 없음
public static void main(String[] args) {
    Stream<Person> personStream = Arrays.asList(
        new Person(21), new Person(30), new Person(16), new Person(20)
    )
    .stream()
    .skip(2);


    System.out.println(personStream.anyMatch(Person::isAdult));
    // 예외 발생함
    personStream.forEach(System.out::println);
}

reduce

  • 스트림의 모든 원소를 하나의 결과로 만들어주는 함수 : 감소연산

첫번째 reduce

  • 어떤 기준에 의해 하나의 원소만 선택함 : 원소 중 가장 큰/작은 원소 찾기
    • Optional reduce(BinaryOperator accumulator);
  • reduce의 인자(함수) : reduce(BinaryOperator) - minBy, maxBy
public static void main(String[] args) {
    Arrays.asList(new Item("바나나우유", 3), new Item("물", 6), new Item("소세지", 10), new Item("불닭볶음면", 6))
		   .stream()
		   .reduce((item1, item2) -> item1.haveMore(item2) ? item1 : item2)
		   .ifPresent(item -> System.out.println(item.getName()));
}

두번째 reduce

  • 초기값을 주고, 하나로 압축시킴
    • 위와 동일한데 초기값을 주는게 다름, 리턴 타입도 옵셔널이 아님 : T reduce(T identity, BinaryOperator<T> accumulator);

세번째 reduce

  • (초기값을 주고, 특정 타입으로 변환한 후, 변환한 타입인 원소를 하나로 모은다)
    • 시그니쳐 : <U> reduce(U identity, BiFunction<U, ? super T, U> accumulator, BiOperator<U> combiner)
int total = items.stream().reduce(0, (sum, item) -> item.totalQuantity(sum), (sum1, sum2) -> sum1 + sum2);
System.out.println(total);