programming study/B-Spring

spring-DI, IOC, OCP, 낮은 결합도 높은 응집도

gu9gu 2022. 9. 14. 18:52

의존성 주입(Dependency Injection, DI)

의존성이 있는 클래스의 인스턴스를 직접 생성하지 않고 spring 컨테이너로부터 생성된 Bean을 주입 받는 것

IoC를 실현시키기 위한 세부적인 개념

 

제어의 역전(Inversion of Control, IoC)

빈 생성, 사용, 삭제 등의 전반적인 제어권을 어플리케이이션이 갖는게 아니라 프래임워크의 컨테이너에게 넘기는 개념

 

개방 폐쇠 원칙(Open-Close Principle, OCP)

모듈은 확장에는 열려있어야 하지만 변경에는 닫혀있어야 한다는 원칙. 즉 기존 코드를 변경하지 않으면서(Close) 기능을 추가(Open)할 수 있도록 설계가 되어야 한다는 원칙입니다.

이 원칙을 지키기 위해서 주로 객체지향의 추상화와 다형성을 활용합니다. 

ex) DB의 save기능을 가진 Oracle, Mysql DB를 모듈을 만들어두고 다른 DB를 추가하는 상황

 -  save 기능을 추상화하여 DB인터페이스를 만듭니다.

 - 다형성 성질을 이용하DB인터페이스를 구현한 클래스 Oracle, Mysql을 만듭니다.

 - DB인터페이스를 구현한 MongoDB클래스만 추가하면 됩니다.

  => 기존 코드들은 변경되지 않았고 새로운 기능은 추가되었습니다.  OCP 원칙을 지킨 것입니다.

 

 

사용하는 이유

결합도를 낮추고 응집도를 높혀서 변경에 유연한 코드를 작성하기 위함.

  • 결합도 - 모듈간 의존 정도, 한 모듈 수정을 위해 다른 모듈의 변경을 요구하는 정도
  • 응집도 - 연관성이 높은 요소들이 모여있는 정도

기능 변경시 코드 수정을 적게 하기 위해 결합도를 낮추고 응집도를 높혀야 한다.

개방 폐쇄 원칙에 따라서 인터페이스, 클래스를 설계하고 제어의 역전 방식으로 의존관계를 설정하면 결합도를 낮추고 응집도를 높힐 수 있다.

 

 

제어의 역전 예시

우선 의존관계를 직접 설정하는 코드입니다.

public class BestStar {

    private Star star;

    public BestStar()
        star = new Star();
        // BestStar 생성자에서 Star 클래스와의 의존관계를 애플리케이션 단에서 직접 설정하고 있다.
    }
}

그런데 개발자가 직접 의존관계를 설정해주는 코드를 지우고 스프링이 제공하는 @Autowired 어노테이션을 사용하면  프레임워크가 BestStar, Star 오브젝트 의존관계를 설정합니다. 

public class BestStar {

    @Autowired
    private Star star;
}

 

 

개방 폐쇄 원칙 예시

 

DB 종류가 변경되더라도 빈 생성하는 코드 부분 한 줄만 수정하면 된다.

      //return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
  • SpringConfig 클래스에서 MemberRepository 빈을 생성한다. 이 때, MemoryMemberRepository, JdbcMemberRepository중 선택해서 빈을 생성할 수 있다.
  • MemberService 클래스가 MemberRepository 인터페이스를 의존하고 있다.
  • MemberRepository 인터페이스를 MemoryMemberRepository, JdbcMemberRepository에서 구현하고 있다.
package hello.hellospring;

import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
public class SpringConfig {
    private DataSource dataSource;

    @Autowired
    public SpringConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        //return new MemoryMemberRepository();
        return new JdbcMemberRepository(dataSource);
    }
}

 

 

package hello.hellospring.service;

import hello.hellospring.repository.MemberRepository;
import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     */
    public Long join(Member member) {
        validateDuplicateMember(member); // 중복 회원 검증

        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m-> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

 

 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository {
    // 동시성 문제로 HashMap 대신 ConcurrentHashMap을 사용해야 함. 나중에 적용해볼까?
    public static Map<Long, Member> store = new HashMap<>();
    // 동시성 문제로 HashMap 대신 AtomicLong 을 사용해야 함. 나중에 적용해볼까?
    public static Long sequence = 0L;

    @Override
    public Member save(Member member) {
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {

        /*
        // 스트림
        long count = IntStream.of(1, 3, 5, 7, 9).count();
        long sum = LongStream.of(1, 3, 5, 7, 9).sum();
        OptionalInt min = IntStream.of(1, 3, 5, 7, 9).min();
        OptionalInt max = IntStream.of(1, 3, 5, 7, 9).max();
        DoubleStream.of(1.1, 2.2, 3.3, 4.4, 5.5)
                .average()
                .ifPresent(System.out::println);

        OptionalInt reduced =
                IntStream.range(1, 4) // [1, 2, 3]
                        .reduce((a, b) -> {
                            return Integer.sum(a, b);
                        });

        List<String> list = List.of("Peter", "Thomas", "Edvard", "Gerhard");
        // print using lambda
        list.forEach(item -> System.out.println(item));
        // print using :: (method reference operator)
        list.forEach(System.out::println);*/

        return store.values().stream()
                .filter(member-> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

    public void clearStore() {
        store.clear();
    }
}

 

 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class JdbcMemberRepository implements MemberRepository {
    private final DataSource dataSource;
    public JdbcMemberRepository(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    @Override
    public Member save(Member member) {
        String sql = "insert into member(name) values(?)";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql,
                    Statement.RETURN_GENERATED_KEYS);
            pstmt.setString(1, member.getName());
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                member.setId(rs.getLong(1));
            } else {
                throw new SQLException("id 조회 실패");
            }
            return member;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional<Member> findById(Long id) {
        String sql = "select * from member where id = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setLong(1, id);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public List<Member> findAll() {
        String sql = "select * from member";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();
            List<Member> members = new ArrayList<>();
            while(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                members.add(member);
            }
            return members;
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    @Override
    public Optional<Member> findByName(String name) {
        String sql = "select * from member where name = ?";
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, name);
            rs = pstmt.executeQuery();
            if(rs.next()) {
                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return Optional.of(member);
            }
            return Optional.empty();
        } catch (Exception e) {
            throw new IllegalStateException(e);
        } finally {
            close(conn, pstmt, rs);
        }
    }
    private Connection getConnection() {
        return DataSourceUtils.getConnection(dataSource);
    }
    private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
    {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (pstmt != null) {
                pstmt.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        try {
            if (conn != null) {
                close(conn);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    private void close(Connection conn) throws SQLException {
        DataSourceUtils.releaseConnection(conn, dataSource);
    }
}

 

 

 

 

 

참고

https://beststar-1.tistory.com/33

 

IoC(Inversion of Control, 제어의 역전) / DI(Dependency Injection, 의존관계 주입)

💡 이 포스팅은 스프링 스터디에 맞게 구성된 것이며, 추후 글의 구조와 다루는 범위가 변경될 수 있으니 참고 바란다. IoC(Inversion of Control, 제어의 역전) 오브젝트 생성, 관계설정, 사용, 제거

beststar-1.tistory.com

https://centbin-dev.tistory.com/entry/%EB%A9%B4%EC%A0%91-%EC%A7%88%EB%AC%B8-DI%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C

 

DI을 사용하는 이유는?

DI - Dependency Injection 의존하는 클래스에 대한 인스턴스를 직접 생성하지 않고, 컨테이너로부터 생성된 빈을 setter나 생성자를 통해 외부로부터 주입받는 것을 의미한다. DI는 왜 사용될까? 가장 큰

centbin-dev.tistory.com

https://creamilk88.tistory.com/137

 

[Framework/Spring/MyBatis/Web] 개념 정리

★ 1. Spring IOC (DI) ★ 2. AOP (Aspect Oriented Programming) 3. MyBatis Framework (영속성 계층 프레임워크) 4. SpringMVC 5. SpringBoot 6. SpringSecurity 높은 응집도(cohension)과 낮은 결합도(coupling..

creamilk88.tistory.com

https://devfunny.tistory.com/8

 

코드의 응집도와 결합도

개방 폐쇄 원칙 (OCP : Open-Closed Principle) 깔끔한 설계를 위해 적용 가능한 객체지향 설계 원칙 중의 하나로, ‘클래스나 모듈은 확장에는 열려있어야하고 변경에는 닫혀있어야한다.’라고 할 수

devfunny.tistory.com

https://gardentree.tistory.com/36

 

스프링 프레임워크 특징

1. 경량(Lightweight) 크기 측면에서 가볍다. 스프링은 여러 개 모듈로 구성되어 있으며, 각 모듈은 하나 이상의 JAR파일로 구성되어 있다. 몇개의 JAR파일만 있으면 개발과 실행이 모두 가능하다. 따

gardentree.tistory.com

https://yoongrammer.tistory.com/97

 

개방-폐쇄 원칙 (OCP: Open-Closed Principle)

목차 개방-폐쇄 원칙 (OCP: Open-Closed Principle) 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다. Bertrand Mayer 계방 폐쇄 원칙은

yoongrammer.tistory.com