객체지향의 사실과 오해 3 ~ 4장

Book Review|2019. 11. 5. 18:12
객체지향의 사실과 오해
국내도서
저자 : 조영호
출판 : 위키북스 2015.06.17
상세보기

3. 타입과 추상화

추상화는 어떤 내용을 명확하게 이해하기 위해 특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다. 그 방법으로 사물간의 차이점을 빼고 공통점을 일반화하여 단순하게 만들고 불필요한 세부사항을 제거하여 단순화 하는 것이다. 즉 복잡성을 이해하기 쉬운 수준으로 단순화하는 것이다.

 

이런 추상화 개념을 이용해서 객체지향에 대입해서 생각해보자.

 

그룹화

각 객체는 명확한 경계를 가지고 서로 구별할 수 있는 구체적인 사람이나 사물을 말한다.

여러 객체에서 공통적인 요소를 뽑아서 그룹화 하면 복잡성을 효과적으로 감소 시킬 수 있다. 예를 들어 토끼, 강아지 등을 동물이라는 공통의 그룹으로 묶고 사과, 딸기 등을 과일이라는 공통의 그룹으로 묶으면 차이점을 빼고 공통점만을 취해 단순화 할 수 있는 추상화의 일종이다.

 

객체지향에서 객체 개개별로 구별하면 좋지만 그렇기에는 너무 이해하기 어려워지기 때문에 공통적인 특성을 뽑아서 하나로 묶는데 이를 묶기위한 공통적인 기반을 개념(concept)라고 한다. 이런 개념을 이용해서 객체를 여러 그룹으로 분류(classification) 할 수 있다. 이렇게 분류가 된 객체를 그룹의 일원이 되었다고 하여 인스턴스(instance)라고 한다.

 

객체 분류의 개념

그럼 객체에서 그룹으로 분류하기 위해 사용되는 개념은 symbol (이름이나 명칭), intension (개념의 정의), extension (개념에 속한 객체들의 집합) 3가지의 관점을 가지고 있다.

 

토끼라는 객체는 동물이라는 개념의 symbol을 사용하였고 4개의 다리와 척추동물이라는 intension을 사용하였고 같이 소속된 돼지나 강아지들을 보면서 extension을 확인하였다. 이로써 토끼는 동물이라는 그룹으로 분류될 수 있게 되었다.

 

객체 분류의 단점

잘못된 객체 분류를 통해 그룹에 이상한 객체가 소속되면 유지보수가 어렵고 변화에 대처하기가 어려워진다.

위의 내용을 종합해보면 객체를 분류하는 건 공통의 요소를 뽑아서 일반화하고 단순화하여 복잡성을 극복하는 작업이다.

 

 

타입

타입은 개념(concept)와 동일한 의미로써 공통점을 기반으로 객체를 묶기위한 틀이다. 객체에서 가장 중요한건 어떠한 행동을 하느냐가 가장 중요하고 그에 따라 타입이 결정된다. 내부에서 표현하는 방식이 서로 다르더라도 행동이 같다면 둘의 타입은 같을 수 있다.

Animal animal1 = new Dog(); 
Animal animal2 = new Cat(); 

animal1.eat(); 
animal2.eat();

위 두개의 객체의 내부 행동은 다르지만 먹는다는 행동이 같기에 같은 타입을 가질 수 있다. 이를 다형성이라고 한다. 이를 통해서 외부의 행동만 제공하고 내부적으로 상세 내용을 숨기는 캡슐화 원칙을 수행 가능하다.

 

이처럼 객체에 타입을 지정할 때는 데이터가 아니라 행동에 따라서 지정 해야 하는 책임 주도 설계를 해야 한다.

 

일반화/특수화 (supertype/subtype)

객체 타입에는 일반적으로 포함되는 타입과 특수한 타입 두 가지로 나뉜다. 강아지로 예를 들어보면 강아지가 짖는건 일반적인 강아지 타입의 특징이다. 하지만 훈련된 애완견의 경우 일반 강아지와 다른 행동이 가능하다. 여기서 강아지라는 특징은 일반적인 것이고 애완견은 특수화된 타입을 의미한다. 여기서 알 수 있듯이 특수화와 일반화의 차이는 행동을 통해서 기반 된다. 특수화된 타입은 일반화 타입의 특징을 포함하고 있으며 일반적인 특징보다 더 적은 모집군을 가지고 있다.

 

이를 슈퍼타입, 서브타입이라고도 하는데 서브타입은 슈퍼티입의 행위에 추가적으로 특수한 행위를 추가한 것이기 때문에 슈퍼타입의 행동은 서브 타입에게 자동으로 상속된다.

 

3장 결론

  • 타입을 통해서 객체를 분류하는데 이 타입은 객체의 행동을 통해서 정해진다. 이렇게 분류한 타입을 이용하여 클래스를 이용하여 프로그램화 한다.

  • 추상화는 객체의 특징을 보고 일반화/특수화하여 불필요한 부분을 제거하고 단순하게 만들어서 분류하는 것이다.

 

4. 역할, 책임, 협력

객체지향에서 협력은 가장 중요한 프레임이다. 협력에 초점을 맞춰서 설계를 하게 되면 저절로 객체들의 행동이 드러나고 그에따라 상태가 결정된다. 객체의 모양을 빚는 것은 객체가 참여하는 협력으로 어떤 협력에 참여하는지가 객체에 필요한 행동을 결정하고, 필요한 행동이 객체의 상태를 결정한다.

 

책임

객체들간의 협력에서 각 객체가 적절한 행동을 해줄 책임을 가지고 있다. 책임은 객체지향의 중요한 재료로써 눙숙한 프로그램 설계는 객체에게 적절하게 책임을 부여해주는 것을 의미한다.

 

이런 책임을 분류하는 기준은 외부에 제공해줄 수 있는 정보와 외부에 제공해 줄 수 있는 서비스에 대한 정의이다. 이런 책임을 바탕으로 객체지향에서 객체는 어떤 객체에 요청 메시지에 따라 어떤 책임 행동을 해줄 수 있는지 결정하는 것이 포인트이다.

 

역할

역할은 객체지향에서 재사용이 가능하고 유연한 설계를 가지는데 중요한 구성요소이다. 동일한 역할을 동일한 책임을 수행할 수 있다는 뜻이다. 또한 동일한 역할을 하는 객체를 추상화하여 유연성을 가지게 하고 재사용성과 단순하게 객체지향을 설계할 수 있다.

 

이 처럼 역할이 추상화되면 상황에 따라 여러 객체를 대체하면서 사용할 수 있어 유연성이 증가한다.

 

협력에 따라 흐르는 객체의 책임

- 설계 시 객체들 사이에 주고받을 요청과 응답의 흐름을 깔끔하게 만들고 (협력 설계) 그 객체들이 협력에 참여하여 자신의 역할을 책임있게 수행해야 한다.

- 즉 객체간의 관계를 먼저 만들고 그 다음 역할과 책임을 설계하라.

 

 

출처 : 객체지향의 사실과 오해 (조영호)

댓글()

JPA 다양한 Join 방법 정리 (N+1, queryDSL, fetch join)

web/Spring|2019. 11. 4. 20:31

JPA를 사용하다 보면 join을 할 때가 많아진다. join을 어떠한 방법으로 하느냐에 따라서 수행되는 쿼리가 달라지고 성능에 문제가 발생하는 경우도 종종있다.

 

그래서 다양한 방식의 join 방식을 알아보고 방식에 따라 작업을 진행해 보자.

우선 사용될 entity 두 개를 설명하면 다음과 같다.

@Getter
@Entity
@Table(name = "wedul_classes")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulClasses extends CommonEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long wedulClassesId;

    @OneToMany(mappedBy = "wedulClasses", fetch = FetchType.LAZY)
    private Set<WedulStudent> wedulStudentList = new LinkedHashSet<>();

    private String classesName;

    private String classesAddr;

}

@Getter
@Entity
@Table(name = "wedul_student")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulStudent extends CommonEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long wedulStudentId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "wedul_classes_id")
    @JsonBackReference
    private WedulClasses wedulClasses;

    private String studentName;

    private int studentAge;

    @Enumerated(value = EnumType.STRING)
    private StudentType studentType;

}

이 두 엔티티는 class와 student의 관계로 1대 N의 관계를 가지고 있다.

이 과정에서 사용될 데이터는 임의로 추가했고 다음과 같다.

 

wedul_classe 테이블의 데이터

wedul_student 테이블의 데이터

사용 쿼리 툴) tadpole docker version

 

 

그럼 이 entity를 이용해서 데이터를 조인하여 조회해보자.

 

1. 단순 조회

우선 첫 번째로 JpaRepository 인터페이스 사용 시 기본적으로 제공하는 findAll을 사용해보자.

@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {

    Optional<WedulClasses> findByClassesName(String classesName);

}

이를 사용하여 데이터를 조회해보면 사용 되는 쿼리는 다음과 같다.

-- classes 목록을 조회하는 쿼리
select
    wedulclass0_.wedul_classes_id as wedul_cl1_0_
    ,wedulclass0_.create_at as create_a2_0_
    ,wedulclass0_.update_at as update_a3_0_
    ,wedulclass0_.classes_addr as classes_4_0_
    ,wedulclass0_.classes_name as classes_5_0_
  from
    wedul_classes wedulclass0_
;


-- 아래 쿼리들은 wedul_classes_id 개수별로 조회되는 쿼리
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;
select
    wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_0_
    ,wedulstude0_.wedul_student_id as wedul_st1_1_1_
    ,wedulstude0_.create_at as create_a2_1_1_
    ,wedulstude0_.update_at as update_a3_1_1_
    ,wedulstude0_.student_age as student_4_1_1_
    ,wedulstude0_.student_name as student_5_1_1_
    ,wedulstude0_.student_type as student_6_1_1_
    ,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
  from
    wedul_student wedulstude0_
  where
    wedulstude0_.wedul_classes_id = ?
;

쿼리를 자세히 보면 알겠지만 wedul_classes를 조회하는 쿼리와 그 wedul_classes 개수만큼 쿼리가 실행되는것을 볼 수 있다.

많이 들어 봤을 법한 N+1 문제가 발생한 것이다.

이 방식으로 쿼리 수행 시 N번의 쿼리가 발생해야 하기에 데이터 수만큼 쿼리가 실행되는 안좋은 부담을 안고 가야해서 좋지 않다.

 

2. left fetch join

위의 1번의 N+1 문제 해결로 고안된 방법 중 하나가 fetch join이다. 나는 left join을 하고자 하기에 left fetch join을 시도해보자. 우선 사용된 코드는 다음과 같다.

@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {
    @Query(value = "select DISTINCT c from WedulClasses c left join fetch c.wedulStudentList")
    List<WedulClasses> findAllWithStudent();
}

distinct가 붙은 이유는 카티션곱에 의해서 여러개의 결과값이 발생해 버리기 때문에 추가하였다.

그럼 사용된 쿼리도 확인해보자.

select
    distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id

left join을 해서 한번에 데이터를 가져올 수 있는 걸 확인 할 수 있지만 아쉽게도 Lazy로 데이터를 가져오지 못하고 Eager로 가져와야 한다.

 

3. EntityGraph

이제 3번째 방식으로 entity graph를 사용하여 실행시켜보자. 코드는 아래와 같다.

@EntityGraph(attributePaths = "wedulStudentList")
@Query("select c from WedulClasses c")
Page<WedulClasses> findEntityGraph(Pageable pageable);

실행되는 쿼리는 다음과 같아서 2번과 동일하다. (page를 사용한 것만 차이)

select
    wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id
  order by
    wedulclass0_.update_at desc;

 

4. QueryDSL

Querydsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 사용할 수 있도록 해주는 프레임워크로 HQL쿼리를 실행하게 도와준다.

설정 방식은 gradle 5 기준으로 다음과 같다.

plugins {
    id 'org.springframework.boot' version '2.2.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id 'java'
}

group = 'com.wedul'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
    maven { url "https://plugins.gradle.org/m2/" }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compile group: "org.flywaydb", name: "flyway-core", version: '5.2.4'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'mysql:mysql-connector-java'
    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
    compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0'
    annotationProcessor 'org.projectlombok:lombok'
    testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // query dsl
    compile("com.querydsl:querydsl-apt")
    compile("com.querydsl:querydsl-jpa")
}

// querydsl 적용
def querydslSrcDir = 'src/main/generated'

querydsl {
    library = "com.querydsl:querydsl-apt"
    jpa = true
    querydslSourcesDir = querydslSrcDir
}

compileQuerydsl{
    options.annotationProcessorPath = configurations.querydsl
}

configurations {
    querydsl.extendsFrom compileClasspath
}

sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', querydslSrcDir]
        }
    }
}

그리고 QueryDsl 사용을 위해 QueryDslRepositorySupport를 상속받아서 사용할 수 있는데 마지막에 distinct를 사용한 것은 2번 fetch 조인의 이유와 동일하다.

@Repository
public class WedulClassesQueryDsl extends QuerydslRepositorySupport {

    public WedulClassesQueryDsl() {
        super(WedulClasses.class);
    }

    public List<WedulClasses> findAllWithStudent() {
        QWedulClasses wedulClasses = QWedulClasses.wedulClasses;
        QWedulStudent wedulStudent = QWedulStudent.wedulStudent;

        return from(wedulClasses)
            .leftJoin(wedulClasses.wedulStudentList, wedulStudent)
            .fetchJoin()
            .distinct()
            .fetch();
    }

}

그럼 마찬가지로 실행되는 쿼리를 확인해보자.

select
    distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
    ,wedulstude1_.wedul_student_id as wedul_st1_1_1_
    ,wedulclass0_.create_at as create_a2_0_0_
    ,wedulclass0_.update_at as update_a3_0_0_
    ,wedulclass0_.classes_addr as classes_4_0_0_
    ,wedulclass0_.classes_name as classes_5_0_0_
    ,wedulstude1_.create_at as create_a2_1_1_
    ,wedulstude1_.update_at as update_a3_1_1_
    ,wedulstude1_.student_age as student_4_1_1_
    ,wedulstude1_.student_name as student_5_1_1_
    ,wedulstude1_.student_type as student_6_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
    ,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
    ,wedulstude1_.wedul_student_id as wedul_st1_1_0__
  from
    wedul_classes wedulclass0_
      left outer join wedul_student wedulstude1_
        on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id

애도 2번, 3번과 동일한 쿼리가 작성되는 걸 확인할 수 있다.

기본적으로 단순하게 다대일 데이터를 가져오려고 하면 N+1 문제가 발생할 수 있기 때문에 조심해야하고 이를 해결하기 위해서는 다양한 방식의 문제 해결 방식이 있는걸 확인할 수 있었다.

무엇이 가장 좋은지는 본인이 판단하거나 상황에 맞게 사용하면 좋을 거 같다.

댓글()

객체지향의 사실과 오해 1 ~ 2장

Book Review|2019. 10. 27. 17:45
객체지향의 사실과 오해
국내도서
저자 : 조영호
출판 : 위키북스 2015.06.17
상세보기

객체지향의 사실과 오해를 읽고 핵심적으로 생각되는 부분만 정리해봤다.

 

1. 협력하는 객체들의 공동체

현실세계의 객체

객체지향을 실세계와 대입하는 경우가 많다. (완벽하게 동일 시 할 수는 없지만 이해하기에는 편리함)

 

그럼 객체 지향을 현실세계에 대입했을 때 커피집을 생각해보면 손님, 캐리어, 바리스타는 개개인의 객체를 의미하고 각 객체는 서로간의 협력관계가 있고 그 속에서 자신의 책임을 다한다.

 

예를 들어보면 손님은 주문을 하고 캐리어는 계산을 받고 바리스타는 커피를 만드는 역할을 한다. 그리고 서로간의 협력 관계를 통해 주문을 하고 받고 커피를 만드는 작업을 진행한다.

 

이렇듯 객체지향에서 가장 중요한 개념은 각자의 역할, 책임 그리고 서로간의 협력이다.

그 중에서 협력은 객체지향에서 중요한 개념으로 서로간의 충분히 협력적이어야하고 다른 객체에 적극적으로 도움을 요청할 정도로 열린 마음을 가져야한다. 여기서의 협력적인 의미는 다른 객체에 수동적이라는 뜻이 아니라 요청에 응답하는 것을 의미하고 어떻게 응답하는 지는 객체 스스로 결정한다. 또한 객체는 충분히 자율적인 존재로 구성된 협력 공동체이다. 협력적일 수 있지만 결국 스스로 행동할 줄 알아야한다.

 

 

객체의 특징

- 객체는 상태와 행동을 가지고 있으며 스스로 자기 자신을 책임진다.

- 객체지향이 절차지향과 다른 가장 큰점은 실행시간에 어떤 행위를 할지 결정하는 것이다. 절차지향의 컴파일 시 결정되는 부분과 가장 큰 다른 점이다.

- 객체의 자율성으로 객체가 외부의 요청을 받는 메소드와 객체가 작업을 하는 구체적인 방법을 나눔으로써 매커니즘이 나눔으로써 매커니즘이 정해지는데 이게 바로 캡슐화이다.

- 객체는 다른 객체와 협력하기 위해 메시지를 전송하고 메시지를 수신한 객체는 메시지를 처리하는 데 적합한 메서드를 자율적으로 선택한다.

- 객체지향은 클래스 기반이 아니라 객체를 중심으로 바라봐야한다. 지나치게 클래스를 강조하는 프로그래밍 언어적인 관점은 객체의 캡슐화를 저해하고 클래스를 서로 강하게 결합시킨다. 어떤 클래스가 필요한가가 아니라 어떤 객체들이 어떤 메시지를 주고받으며 협력하는가를 중요시 여기자.

- 클래스의 구조와 메서드가 아니라 객체의 역할, 책임, 협력에 집중하라.

 

 

2. 이상한 나라의 객체

객체는 인간이 분명하게 구별할 수 있는 물리적인 또는 개념적인 경계를 지닌 어떤 것을 의미한다. 객체는 현실세계와 정확하게 같지 않고 모방하는 것이다. 그리고 그 현실세계를 기반으로 새로운 세계를 창조하는 것이 객체지향이다.

 

객체에 있는 상태를 특정시점에 객체가 가지고 있는 정보의 집합이다. 객체의 상태는 객체에 존재하는 정적인 프로퍼티와 동적인 프로퍼티로 구성된다. 프로퍼티는 단순한 값을 나타내는 속성과 다른 객체를 참조하는 Link로 구성된다. 

 

객체는 자율적인 존재로써 다른 객체가 값을 바꿀수 없다. 하지만 객체의 행동은 상태에 영향을 받고 변경시키는데 이러한 행동은 외부의 요청 또는 수신된 메시지에 응답하기 위해 동작하고 반응하는 활동이다. 행동의 결과로 객체는 자신의 상태를 변경하거나 다른 객체에 메시지를 전달할 수 있다.

 

객체의 상태는 캡슐속에 감춰두고 행동만 외부로 노출시켜서 그 행동으로 상태가 변경될 수 있도록 하는 것, 객체가 주체가 되어 행동하는 것을 캡슐화라고 한다.

 

 

 

1장과 2장에서 객체지향속에서 객체의 정확한 정의를 다시 정리할 수 있었던 것 같다. 

단순하게 코드로 보여주는게 아니라 이야기로 객체지향에 대해 이해하면서 볼 수 있어서 좋았다.

좋은 책이다.

 

출처 : 객체지향의 사실과 오해 (조영호)

댓글()

nginx 서버에 filebeat를 이용하여 ELK에 로그 기록하기

IT 지식/Docker|2019. 10. 15. 22:00
git clone https://github.com/deviantony/docker-elk

nginx를 설치하고 docker 기반으로 ELK (elasticsearch, logstash, kibana)를 설치하고 nginx 로그를 filebeat를 설치하여 acces.log, error.log, syslog등을 전송해보자.

 

설치 

ELK를 도커에 설치하는 스크립트를 아래 github에 잘 정리되어 제공해주고 있다.
https://github.com/deviantony/docker-elk

ELK는 이걸로 설치하면 되는데 docker-compose로 nginx와 filebeat까지 함께 설치하기 위해서 아래 저장소에서 제공하는 nginx-filebeat 스크립트를 혼합해서 사용해보자.
https://github.com/spujadas/elk-docker/tree/master/nginx-filebeat

1. 우선 ELK 설치 스크립트를 가져오자.

git clone https://github.com/deviantony/docker-elk



2. 그리고 nginx-filebeat 파일을 다운 받아서 docker-elk 디렉토리 내부에 추가한다.

git clone https://github.com/spujadas/elk-docker
mv ./elk-docker/nginx-filebeat ./docker-elk



3. 그리고 nginx-filebeat까지 사용할 수 있도록 docker-compose.yml 스크립트를 수정해준다. 그리고 nginx.conf 파일을 쉽게 보고 생성된 log도 로컬에서 보기 위해서 mount를 로컬 폴더로 진행한다.

version: '3.2'

services:
  elasticsearch:
    container_name: "elasticsearch"
    build:
      context: elasticsearch/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./elasticsearch/config/elasticsearch.yml
        target: /usr/share/elasticsearch/config/elasticsearch.yml
        read_only: true
      - type: volume
        source: elasticsearch
        target: /usr/share/elasticsearch/data
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      ES_JAVA_OPTS: "-Xmx256m -Xms256m"
      ELASTIC_PASSWORD: changeme
    networks:
      - mynet

  logstash:
    container_name: "logstash"
    build:
      context: logstash/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./logstash/config/logstash.yml
        target: /usr/share/logstash/config/logstash.yml
        read_only: true
      - type: bind
        source: ./logstash/pipeline
        target: /usr/share/logstash/pipeline
        read_only: true
    ports:
      - "5000:5000"
      - "9600:9600"
    environment:
      LS_JAVA_OPTS: "-Xmx256m -Xms256m"
    networks:
      - mynet
    depends_on:
      - elasticsearch

  kibana:
    container_name: "kibana"
    build:
      context: kibana/
      args:
        ELK_VERSION: $ELK_VERSION
    volumes:
      - type: bind
        source: ./kibana/config/kibana.yml
        target: /usr/share/kibana/config/kibana.yml
        read_only: true
    ports:
      - "5601:5601"
    networks:
      - mynet
    depends_on:
      - elasticsearch

  nginx:
    container_name: "nginx"
    build:
      context: nginx-filebeat/
    volumes:
      - type: bind
        source: /Users/we/Documents/docker/nginx
        target: /etc/nginx
      - type: bind
        source: /Users/we/Documents/docker/nginx_log
        target: /var/log
    ports:
      - "8080:80"
    networks:
      - mynet

networks:
  mynet:
    driver: bridge

volumes:
  elasticsearch:

 

4. 마지막으로 filebeat.xml에서 ssl 통신을 하지 않기 때문에 ssl 부분을 제거해준다. (이유는 하단에 나온다.)

output:
  logstash:
    enabled: true
    hosts:
      - logstash:5044
    timeout: 15

filebeat:
  inputs:
    -
      paths:
        - /var/log/syslog
        - /var/log/auth.log
      document_type: syslog
    -
      paths:
        - "/var/log/nginx/*.log"
      document_type: nginx-access

 

5. 그럼 지금까지 수정한 내용을 이용해서 docker-compose up -d 통해 설치 진행해보자. 설치후 제거 하고 싶으면 (docker-compose -v down) 명령어를 통해 제거 할 수 있다.

docker-compose up -d

 

6. 설치가 완료되면 키바나에 접속해서 확인해보면 logstash, elasticsearch, kibana 모두 설치 된 것을 알 수 있다. 초기 계정은 elastic / changeme이다.

ELK와 nginx docker process

7. logstash pipeline을 만들어줘야하는데 kibana에서 management → logstash → pipeline에서 설정해주면된다. 간단하게 5044포트로 받고 nginx_log 인덱스로 넣게 설정한다.

input {
    beats {
        client_inactivity_timeout => 19909
        port => "5044"
        ssl => false
    }
}
filter {
  if [type] == "nginx-access" {
    grok {
      match => { "message" => "%{NGINXACCESS}" }
    }
  }
}
output {
    elasticsearch {
        hosts => ["elasticsearch:9200"]
        index => "nginx_log"
        user => "elastic"
        password => "changeme"
    }
   stdout { codec => rubydebug }
}

 

8. 그럼 filebeat가 정상적으로 동작하는지 확인해보자.

filebeat 실행 상태 확인

/etc/init.d/filebeat status

 

filebeat.xml 기준으로 설정 정상 적용 상태 확인

filebeat test config

 

filebeat.xml에 설정된 output 정상 여부 확인

filebeat test output

filebeat 테스트 결과

위에 테스트를 진행하면 위에 화면처럼 나오는게 나와야 정상이다. 정상적으로 가동된걸 확인했다.

그럼 실제 nginx에서 나온 로그가 filebeat로 수집되어 logstash > elasticsearch로 정상적으로 적재되는지 보자.
localhost:8080에 접속하여 로그 발생시킨 후 filebeat 로그를 확인했는데 왠걸 다음과 같은 오류가 발생했다.

[2019-10-15T06:12:48,844][INFO ][org.logstash.beats.BeatsHandler] [local: 172.28.0.4:5044, remote: 172.28.0.2:55946] Handling exception: org.logstash.beats.BeatsParser$InvalidFrameProtocolException: Invalid Frame Type, received: 1
[2019-10-15T06:12:48,845][WARN ][io.netty.channel.DefaultChannelPipeline] An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
io.netty.handler.codec.DecoderException: org.logstash.beats.BeatsParser$InvalidFrameProtocolException: Invalid Frame Type, received: 1
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:459) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.handler.codec.ByteToMessageDecoder.channelInputClosed(ByteToMessageDecoder.java:392) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.handler.codec.ByteToMessageDecoder.channelInputClosed(ByteToMessageDecoder.java:359) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.handler.codec.ByteToMessageDecoder.channelInactive(ByteToMessageDecoder.java:342) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelInactive(AbstractChannelHandlerContext.java:245) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.channel.AbstractChannelHandlerContext.access$300(AbstractChannelHandlerContext.java:38) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.channel.AbstractChannelHandlerContext$4.run(AbstractChannelHandlerContext.java:236) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.util.concurrent.DefaultEventExecutor.run(DefaultEventExecutor.java:66) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:858) [logstash-input-tcp-6.0.3.jar:?]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [logstash-input-tcp-6.0.3.jar:?]
at java.lang.Thread.run(Thread.java:834) [?:?]
Caused by: org.logstash.beats.BeatsParser$InvalidFrameProtocolException: Invalid Frame Type, received: 1
at org.logstash.beats.BeatsParser.decode(BeatsParser.java:92) ~[logstash-input-beats-6.0.0.jar:?]
at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489) ~[logstash-input-tcp-6.0.3.jar:?]
at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:428) ~[logstash-input-tcp-6.0.3.jar:?]
... 10 more

이 문제는 logStash 또는 filebeat가 동시에 ssl을 사용하지 않는데 한쪽만 ssl 통신을 했을 때 발생되는 오류이다.

그래서 아까 위에 filebeat에 ssl 부문을 지우고 pipeline에 ssl 설정을 false로 지정한 것이다. 그럼 다시한번 localhost:8080에 들어가보자.

지금은 index.html을 만들어 놓지 않아서 404 에러가 발생된다.

GET nginx_log/_search
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "nginx_log",
        "_type" : "_doc",
        "_id" : "vc04zm0BZBO8vgBrBmV7",
        "_score" : 1.0,
        "_source" : {
          "ecs" : {
            "version" : "1.1.0"
          },
          "@version" : "1",
          "tags" : [
            "beats_input_codec_plain_applied"
          ],
          "message" : """2019/10/15 07:00:31 [error] 25#25: *16 "/etc/nginx/html/index.html" is not found (2: No such file or directory), client: 172.28.0.1, server: , request: "GET / HTTP/1.1", host: "localhost:8080"""",
          "host" : {
            "name" : "a31d3333d22b"
          },
          "agent" : {
            "type" : "filebeat",
            "version" : "7.4.0",
            "hostname" : "a31d3333d22b",
            "id" : "cc4eb582-e09c-4a83-bb2e-9721c39ee508",
            "ephemeral_id" : "a5918a0b-4688-458f-bbc2-4eb49a3fff03"
          },
          "log" : {
            "file" : {
              "path" : "/var/log/nginx/error.log"
            },
            "offset" : 8524
          },
          "@timestamp" : "2019-10-15T07:00:38.443Z"
        }
      },
      {
        "_index" : "nginx_log",
        "_type" : "_doc",
        "_id" : "vs04zm0BZBO8vgBrBmV8",
        "_score" : 1.0,
        "_source" : {
          "ecs" : {
            "version" : "1.1.0"
          },
          "@version" : "1",
          "tags" : [
            "beats_input_codec_plain_applied"
          ],
          "message" : """172.28.0.1 - - [15/Oct/2019:07:00:31 +0000] "GET / HTTP/1.1" 404 555 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36"""",
          "host" : {
            "name" : "a31d3333d22b"
          },
          "agent" : {
            "type" : "filebeat",
            "version" : "7.4.0",
            "hostname" : "a31d3333d22b",
            "id" : "cc4eb582-e09c-4a83-bb2e-9721c39ee508",
            "ephemeral_id" : "a5918a0b-4688-458f-bbc2-4eb49a3fff03"
          },
          "log" : {
            "file" : {
              "path" : "/var/log/nginx/access.log"
            },
            "offset" : 8528
          },
          "@timestamp" : "2019-10-15T07:00:38.443Z"
        }
      }
    ]
  }
}

 

정상적으로 적재가 잘되는것을 확인할 수 있다.

이로써 docker로 구성된 elk로 로그 적재를 진행해봤다. 

 

관련 패키지는 github에 올려놓았다.

https://github.com/weduls/elk_with_nginx

댓글()

우아한 객체지향 후기 및 정리

DDD|2019. 10. 12. 19:23

https://www.youtube.com/watch?v=dJ5C4qRqAgA

우아한 형제들에서 진행한 우아한 객체지향 세미나에 가고 싶었는데 아쉽게도 가지 못했다. 발표해주시는분이 객체지향의 사실과 오해를 쓰신 조영호분이라서 더 가보고 싶었는데 아쉽다. 책에 내용이 좋아서 동영상으로라도 보고 싶었는데 유튜브에 동영상이 올라와서 보고 정리해본다.

 

개념

- 설계는 코드에 어디에 놓을건지를 정하는 것.

- 의존성 문제의 핵심은 코드 변경시 영향을 주는지이다.

- 의존성 문제는 디커플링이 되어야 한다.

 

관계설명

연관관계 (Association)

A 클래스에 B클래스로 갈수 있는 영구적인 방법이 있는 경우

A → B

class A { 
	private B b;
}

 

의존관계 (Dependency)

A ---> B (파라미터, 리턴타입, 지역변수 등등)

일시적으로 관계를 맺고 사라지는 관계

class A { 
	public B method(B b) { 
		return new B(); 
	}
}

 

상속관계 (Inheritance)

A → B

부모에 구현이 바뀌면 영향을 받을 수 있음.

public class A extends B

 

실체화 관계 (Realization)

인터페이스에 오퍼레이션 시그니처가 변경되었을때만 영향을 받음

A ---> B

class A implements B

 

효율적인 객체 설계법

1. 양향향 의존성을 피하라

  • 순환참조를 피하라. 오직 단방향 참조를 하라.
  • 다중성이 적은 방향을 선택하라.
  • 1대다 보다 다대1로 설계하라. (아래 예시)
class A { 
	private Collection<B> bs;
}
보다

class B { 
	private A a
}
로 사용하라.

 

2. 제일 좋은건 의존성을 줄여라.

  • 관계가 있다는 것은 파라미터, 또는 인스턴스 변수등에서 전달되는 사이가 있다.
  • 관계에는 방향성이 있어야 한다.
  • 협력의 방향, 의존성의 방향
  • 상속관계랑 실체화 관계의 경우 명확하고 대부분이 연관, 의존 관계가 대부분
  • 데이터 흐름의 의존적일 수 있다.
  • 영구적인지, 일시적인지 여부에 따라서 의존관계 연관관계를 지정
  • 관계는 런타임에 어떤 연관을 가지는지에 따라 달라진다.

 

3.  연관관계 정의

  • 이 객체를 알면 다른 객체를 찾아갈수 있어요가 연관관계의 정의
  • 설계를 할때 의존관계를 손으로 그리고 코드를 작성한다. => 잘못된 패키지 참조가 발생되나?
  • 도메인에 나눠서 패키지를 구성하라.
  • 레이어 구현은 패키지
  • 패키지내부에 어떤 레이어로 나눌건지 패키지 구성
  • 의존성 역전 원칙
  • 패키지 싸이클은 절대 돌면 안된다.

 

대표적인 연관성 문제 해결방안

1. 객체 참조

  • 객체 참조로 List<B> b로 데이터를 가지고 있으면 메모리에 있을때 큰 이슈는 없으나 orm을 통해 데이터를 읽을 때 문제의 소지 발생 가능 (N+1 문제 등등)
  • 객체의 연관성이 너무 얽혀있음.
  • 객체 참조로 많은걸 수정하면 롱 트랜잭션이 생겨서 트랜잭션의 경계가 모호해짐.
  • 객체의 연관성이 높아지면 트랜잭션이 포인트가 커져서 트랜잭션의 문제가 발생
  • 객체 참조는 연관성이 너무 크기 때문에 필요하다면 다 끊어야한다.

  • 모든 객체 참조가 불 필요한 건 아니다. 함께 생성되고 함꼐 삭제되는 객체는 묵어라. 다시 말해 도메인 제약 사항을 공유하는 객체들은 묶어라. (도메인 관점으로 묶어라). 가능하면 분리하라 
  • 아래 같은 경우처럼 같은일은 하는 Order, OrderLineItem, OrderOptionGroup, OrderOption의 연관관계는 유지하고 Shop은 연관관계를 끊고 shopId를 보유하고 그 Id를 이용해서 데이터를 탐색하라.

  • 같은 객체와 연관관계가 있는 객체는 같은 트랜잭션 레벨에서 관리하면 된다. (같은 객체에 정보는 한번에 쿼리로 가져온면 된다.)

 

2. 객체지향보다 절차지향이 더 좋을때도 있다.

만약 연관관계가 있어서 사용하다가 연관관계를 끊으면서 문제가 생기면 그 부분을 별도의 로직으로 분리한다. 예를 들어 Order에서 validation을 밖으로 빼어 OrderValidatiuon을 만들면 Order에서 Shop, Menu등에 대한 연관관계가 필요없어진다. 그리고 Order에 별도의 validation 코드가 있다보면 응집도가 높은 코드들이 아닌 코드들이 같이 있기 때문에 통일성이 부족해진다. 꼭 객체안에 validation을 체크하는 로직이 같이 있을 필요는 없다.

 

3. 문제상황

의존성을 없애기 위해서 Shop객체가 사라지고 shopId가 생기면서 배달완료되고 배달료에 부여하는 코드에서 컴파일 오류가 발생된다.

이 부분을 validation처리 처럼 절차지향으로 변경하거나 도메인 이벤트 퍼블리싱으로 해결할 수 있다.

 

1) 절차지향 방식 사용

OrderDeliveredService를 사용해서 orderId를 받아서 Shop에 비용을 부과함으로써 Order에서 Shop의 연관성을 제거할 수 있다. 

하지만 DorderDeliveredService에 Order, Shop, Delivery를 사용하기 때문에 의존성이 다시 생긴다.

그래서 Interface를 이용해서 의존성을 해결해주면 된다. 

 

2) Domain Event를 이용한 의존성 제거

스프링에서 제공하는 Domain Event를 이용하여 특정 이벤트가 발생했을때 기능을 발생시키도록 하면 의존성없이 해결할 수 있다. 간단하게 AbstractAggregateRoot를 상속받아서 registerEvent를 통해 이벤트를 등록하면 핸들러가 처리한다. 실제로 배달의 민족의 대부분의 서비스가 다음과 같은 방식으로 이루어져있다고 한다.

이벤트가 발생하면 handler에서 이벤트를 받아서 비동기로 데이터를 처리하면 끝.!

 

직접 듣는게 아니라 녹화된 방송으로 들어서 정리하고 이해하면서 들을 수 있어서 더 좋았다. 다음에는 실제로 참석해서 들어보고 싶다. 개발하면서 지켜가면서 개발을 해봐야겠다.

댓글()

데이터 베이스 버전 컨트롤 Flyway

web/Spring|2019. 9. 28. 13:43

Spring에서 초기 테이블과 데이터 관리를 위해서 data.sql과 schema.sql을 사용하였다. 하지만 테이블 스키마가 변경되거나 필수로 초기에 들어가야하는 데이터들이 추가되거나 수정되었을 때 히스토리 관리가 잘 되지 않았다. 

특히 서로 교류가 잘 되지 않은 경우에서는 컬럼이 추가되거나 무엇이 변경되었는지 알지 못해서 문제를 유발할 수 있기에 이를 관리 할 수 있는 무언가가 필요했다.

그래서 Redgate에서 제공하는 Flyway를 사용해보기로 했다. 우선 내 개인 프로젝트인 timeline에 적용시켜봤다.

 

데이터베이스 버전관리 Flyway

https://flywaydb.org/

동작 방식

Flyway가 버전관리를 하기위해서 테이블이 생성된다. Flyway가 버전관리는 이 테이블에 데이터베이스의 상태를 기록하면서 진행한다. 

Flyway가 시작되면 파일시스템 또는 마이그레이션 대상의 classpath를 스캔해서 Sql 또는 Java로 쓰여진 파일을 찾는다. 이 마이그레이션 작업은 파일에 적혀있는 version number대로 순서대로 진행된다. 그리고 현재 마이그레이션 해야할 파일의 버전과 테이블에 기록된 버전을 확인해보고 같으면 넘어간다.

Flyway에서 사용하는 테이블은 flyway_schema_history로 아래와 같이 구성되어있다.

CREATE TABLE `flyway_schema_history` (
  `installed_rank` int(11) NOT NULL,
  `version` varchar(50) DEFAULT NULL,
  `description` varchar(200) NOT NULL,
  `type` varchar(20) NOT NULL,
  `script` varchar(1000) NOT NULL,
  `checksum` int(11) DEFAULT NULL,
  `installed_by` varchar(100) NOT NULL,
  `installed_on` timestamp NOT NULL DEFAULT current_timestamp(),
  `execution_time` int(11) NOT NULL,
  `success` tinyint(1) NOT NULL,
  PRIMARY KEY (`installed_rank`),
  KEY `flyway_schema_history_s_idx` (`success`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
installed_rank 인덱스
version 버전명 (V나 R 뒤에 붙는 숫자)
description 설명
type SQL 또는 JDBC 
script 스크립트 이름 V1__kdjlkdf.sql
checksum checksum
installed_by 실행 주최자
installed_on 설치된 시간
execution_time 총 실행시간
success 성공여부

 

간단히 말해 변경된 데이터나 테이블 스키마를 적용하기 위해서는 마지막 버전보다 높은 파일을 만들어서 애플리케이션을 구동하면 된다.

 

애플리케이션에 적용

그럼 flyway를 적용하기 위해 gradle에 라이브러리부터 추가해보자.

dependency {
	compile group: "org.flywaydb", name: "flyway-core", version: '5.2.4'
}

그리고 application.yml을 설정하자.

spring:
  flyway:
    enabled: true
    baselineOnMigrate: true
    encoding: UTF-8

그리고 테이블과 데이터를 넣을 sql을 만들자. 

그리고 Springboot 애플리케이션을 실행시키면 해당 테이블에 버전 히스토리가 기록된다.

 

버전관리하기에 좋은거 같다.

댓글()