Home

Published

- 15 min read

IOC(Inversion Of Control)이란?

img of IOC(Inversion Of Control)이란?

IOC를 제대로 이해하려면, 기존의 방식에서 어떤 문제가 있었고 뭐가 문제였는지를 알 필요가 있다고 판단했다. 따라서, IOC를 사용하지 않은 기존 방식을 코드로 이해하고 어떤 점이 바뀌었는지, 그리고 IOC가 어떤 식으로 적용되었는지 알아보자.

간단한 웹서버를 하나 만들어보자.

간단한 웹서버(IOC를 사용하지 않음)

   // 데이터베이스 접근 클래스
class UserDao {
    public User getUser(String userId) {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
        "jdbc:mysql://localhost/user", "user", "name");
        
        PreparedStatement ps = c.preparestatement(
        "select * from users where id = ?");
        ps.setString(1, id);
        
        ResultSet rs = ps.executeQuery();
        rs.next();
        
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        return user;
    }
}

// 비즈니스 로직을 처리하는 서비스 클래스
class UserService {
    private UserDao userDao;

    public UserService() {
        // UserService가 직접 UserDao 객체를 생성
        this.userDao = new UserDao();
    }

    public String getUserName(String userId) {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        UserService userService = new UserService();
        String userName = userService.getUserName("12345");
        System.out.println("User Name: " + userName);
    }
}

특징들을 하나 하나 클라이언트부터 살펴보자.

Client Class

   // 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        UserService userService = new UserService();
        String userName = userService.getUserName("12345");
        System.out.println("User Name: " + userName);
    }
}

클라이언트에서는 유저 서비스를 불러와 유저 이름을 불러온다. main에서 동작한다. 그러면 서비스 내부는 어떻게 구성되었는지 살펴보자.

Service Class

   // 비즈니스 로직을 처리하는 서비스 클래스
class UserService {
    private UserDao userDao;

    public UserService() {
        // UserService가 직접 UserDao 객체를 생성
        this.userDao = new UserDao();
    }

    public String getUserName(String userId) {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

유저 서비스 생성자를 살펴보면 UserDao 객체를 직접 생성하는 것을 볼 수 있다. 그리고 유저 Dao에서 유저를 불러온다. 유저 Dao를 살펴보자.

   // 데이터베이스 접근 클래스
class UserDao {
    public User getUser(String userId) {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection(
        "jdbc:mysql://localhost/user", "user", "name");
        
        PreparedStatement ps = c.preparestatement(
        "select * from users where id = ?");
        ps.setString(1, id);
        
        ResultSet rs = ps.executeQuery();
        rs.next();
        
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        return user;
    }
}

유저 Dao를 보면 데이터베이스에서 유저를 가져오는 것을 볼 수 있다.

문제점

그래서 개발자가 객체를 설계할 때 가장 염두에 두어야 할 사항은 바로 미래의 변화를 어떻게 대비할 것인가이다. 지금 당장 구현하고 있는 기능도 만들기 바쁜데 무슨 미래를 생각할 여유가 있겠느냐고 반문할지 모르겠다. 맞는 말이다. 하지만 지혜로운 개발자는 오늘 이 시간에 미래를 위해 설계하고 개발한다. 그리고 그 덕분에 미래에 닥칠지도 모르는 거대한 작업에 대한 부담과 변경에 따른 엄청난 스트레스, 그로 인해 발생하는 고객과의 사이에서 또 개발팀 내에서의 갈등을 최소화할 수 있다.변화는 먼 미래에만 일어나는 게 아니다. 며칠 내에, 때론 몇 시간 후에 변화에 대한 요구가 갑자기 발생할 수 있다. 객체지향 설계와 프로그래밍이 이전의 절차적 프로그래밍 패러다임에 비해 초기에 좀 더 많은, 번거로운 작업을 요구하는 이유는 객체지향 기술 자체가 지니는, 변화에 효과적으로 대처할 수 있다는 기술적인 특징 때문이다. 객체지향 기술은 흔히 실세계를 최대한 가깝게 모델링할 수 있기 때문에 의미가 있다고 여겨진다. 하지만 그보다는 객체지향 기술이 만들어내는 가상의 추상세계 자체를 효과적으로 구성할 수 있고, 이를 자유롭고 편리하게 변경, 발전, 확장시킬 수 있다는 데 더 의미가 있다.토비의 스프링 vol 1

그래서 위 코드가 어떤 문제가 발생할 수 있는지 하나하나 뜯어보자.

1. 높은 결합도

   // 비즈니스 로직을 처리하는 서비스 클래스
class UserService {
    private UserDao userDao;

    public UserService() {
        // UserService가 직접 UserDao 객체를 생성
        this.userDao = new UserDao();
    }

    public String getUserName(String userId) {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

코드를 보면 UserServiceUserDao의 구체적인 구현에 직접 의존하고 있다. 만약 회사에서 지금 사용하고 있는 데이터베이스인 MySQL 대신 Oracle 데이터베이스로 전환하기로 결정했다고 가정해보자. 기존 코드에서는 UserDao가 MySQL에 특화된 코드를 포함하고 있어, UserService까지 수정해야 한다.

그래서 UserDao 내부 코드도 바뀌어야 하고, UserServiceUserDao 생성 부분도 수정하고, 만약 다른 서비스도 이런 식으로 작성했다면 모두 수정해야 한다. 정신이 나갈 것 같다. 이처럼 높은 결합도로 코드를 작성하면 문제가 생긴다. 결합도를 낮춰야 한다.

2. 객체 생성 책임의 집중

현재 UserServiceUserDao의 생성을 직접 담당하고 있다. 만약 회사에서 로깅 기능을 추가해야 한다고 가정해보자.

   class UserDao { 
	private Logger logger; 
	
	public UserDao(Logger logger) { 
		this.logger = logger; 
	} 
	// ... 나머지 코드 
}

이렇게 된다면 서비스도 다음과 같이 수정해야 한다.

   class UserService {
    private UserDao userDao;
    
    public UserService() {
        Logger logger = new Logger();
        this.userDao = new UserDao(logger);
    }
    // ... 나머지 코드
}

이런 방식으로 UserDao를 사용하는 모든 클래스를 변경해야 한다. 또한 UserServiceLoggerUserDao 둘 모두의 생성을 책임지고 있다. 이건 단일 책임의 원칙에서 위배된다.

SOLID

SOLID는 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙을 마이클 페더스(Michael Feathers)가 소개한 약어로, 로버트 마틴(Robert C. Martin)이 2000년대 초반에 명명한 원칙들을 가리킨다.

  1. S - Single Responsibility Principle (단일 책임 원칙) : 한 클래스는 하나의 책임만 가져야 한다.
  2. O - Open-Closed Principle (개방-폐쇄 원칙) : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  3. L - Liskov Substitution Principle (리스코프 치환 원칙) : 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  4. I - Interface Segregation Principle (인터페이스 분리 원칙) : 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  5. **D - Dependency Inversion Principle (의존관계 역전 원칙) **: 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.

3. 유연성 부족

1번에서 이야기했던 높은 결합성으로 인한 문제로, 유연성이 떨어져 다른 데이터베이스나 파일 시스템을 사용한다고 요구사항이 바뀌게 되면 문제가 생긴다.

리팩토링(문제 해결)

앞서 말했던 3가지 문제들을 해결하기 위해, 수정된 코드를 보자.

   // 데이터베이스 접근 인터페이스
interface UserDao {
    User getUser(String userId) throws SQLException;
}

// 실제 MySQL 데이터베이스 구현
class MySqlUserDao implements UserDao {
    public User getUser(String userId) throws SQLException {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        
        try (Connection c = DriverManager.getConnection(
                "jdbc:mysql://localhost/user", "user", "name");
             PreparedStatement ps = c.prepareStatement(
                "select * from users where id = ?")) {
            
            ps.setString(1, userId);
            
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    User user = new User();
                    user.setId(rs.getString("id"));
                    user.setName(rs.getString("name"));
                    return user;
                } else {
                    throw new SQLException("User not found");
                }
            }
        }
    }
}

// 비즈니스 로직을 처리하는 서비스 클래스
class UserService {
    private UserDao userDao;

    // 생성자를 통한 의존성 주입
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public String getUserName(String userId) throws SQLException {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

// IOC 컨테이너 역할을 하는 설정 클래스
class AppConfig {
    public UserDao userDao() {
        return new MySqlUserDao();
    }

    public UserService userService() {
        return new UserService(userDao());
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        UserService userService = appConfig.userService();
        try {
            String userName = userService.getUserName("12345");
            System.out.println("User Name: " + userName);
        } catch (SQLException e) {
            System.err.println("Error retrieving user: " + e.getMessage());
        }
    }
}

UserDao

   interface UserDao {
    User getUser(String userId) throws SQLException;
}

class MySqlUserDao implements UserDao {
    public User getUser(String userId) throws SQLException {
        // 데이터베이스 접근 로직
    }
}

기존에는 UserDao에서 데이터베이스에 접근했었다. 하지만 이제 UserDao를 인터페이스로 바꾸고, UserDao를 구현하는 구현체로서 MySqlUserDao를 생성했다.

UserService

   class UserService {
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public String getUserName(String userId) throws SQLException {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

이제는 UserService가 UserDao를 생성하지 않는다. 보면 생성자를 통해 외부에서 주입받는 걸 알 수 있다.(의존성 주입)

AppConfig

   class AppConfig {
    public UserDao userDao() {
        return new MySqlUserDao();
    }

    public UserService userService() {
        return new UserService(userDao());
    }
}

이 클래스는 새로 만든 클래스로, UserDaoUserService의 인스턴스를 생성하고 조립하는 책임을 집중화했다.

IOC(Inversion Of Control)이란?

IOC란 영어 그대로 제어의 역전이다. 제어의 역전이라는 말은 무슨 의미일까? 예전에는 객체가 객체의 생성을 제어하는 방식이었다. 코드로 보자.

   class UserService {
    private UserDao userDao;

    public UserService() {
        // UserService가 직접 UserDao 객체를 생성
        this.userDao = new UserDao();
    }

    public String getUserName(String userId) {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

보면 UserServiceUserDao를 “제어”하는 것으로 볼 수 있다. 하지만 이런 방식은 문제가 많았고, 다음 방식으로 바뀌게 되었다.

   class UserService {
    private UserDao userDao;

    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }

    public String getUserName(String userId) throws SQLException {
        User user = userDao.getUser(userId);
        return user.getName();
    }
}

이제는 UserDao를 외부에서 “주입” 받는다. 이를 통해 UserService가 제어를 하지 않고, 다른 객체(클래스)에서 UserDao를 제어한다.

   class AppConfig {
    public UserDao userDao() {
        return new MySqlUserDao();
    }

    public UserService userService() {
        return new UserService(userDao());
    }
}

이런 방식으로 바뀜과 동시에 객체의 생성과 생명주기 관리의 책임이 AppConfig로 이전하였다.

이렇게 IOC를 통해

  1. 클래스들은 자신의 핵심 기능에만 집중할 수 있게 된다.
  2. 의존성 관리가 중앙화되어 유지보수가 쉬워진다.
  3. 각 컴포넌트의 결합도가 낮아져 유연성과 확장성이 증가한다.

제어의 역전 개념은 사실 이미 폭넓게 적용되어 있다. 서블릿을 생각해보자. 일반적인 자바 프로그램은 main() 메소드에서 시작해서 개발자가 미리 정한 순서를 따라 오브젝트가 생성되고 실행된다. 그런데 서블릿을 개발해서 서버에 배포할 수는 있지만, 그 실행을 개발자가 직접 제어할 수 있는 방법은 없다. 서블릿 안에 main() 메소드가 있어서 직접 실행시킬 수 있는 것도 아니다. 대신 서블릿에 대한 제어 권한을 가진 컨테이너가 적절한 시점에 서블릿 클래스의 오브젝트를 만들고 그 안의 메소드를 호출한다. 이렇게 서블릿이나 JSP, EJB처럼 컨테이너 안에서 동작하는 구조는 간단한 방식이긴 하지만 제어의 역전 개념이 적용되어 있다고 볼 수 있다.토비의 스프링 vol1

참고:

  • 토비의 스프링 vol 1