728x90

@Configuration을 사용하는 것과 그냥 팩토리를 사용하는 것과 다른 것이 없어보인다.

 

만약 팩토리를 사용하여 userDao() 메서드를 여러번 호출한다면 동일한 오브젝트가 돌아올까?

당연히 아니고, 매번 새로운 오브젝트가 생성되어 다른 오브젝트가 만들어진다.

 

하지만 애플리케이션 컨텍스트에서 getBean()을 사용해 가져오는 객체는 모두 동일한 오브젝트를 가져오게 된다.

싱글톤으로 저장하고 관리하기 때문이다.

 

왜 스프링에서는 빈을 싱글톤으로 관리할까?

스프링은 대규모의 요청을 처리하기 위해 만들어졌다.

만약 싱글톤을 사용하지 않는다면 요청마다 오브젝트를 생성해야 하고, 생성에도 비용이 드는 것이 아니라 가비지 컬렉션에도 큰 비용이 들게 될 것이다.

그렇기에 하나의 오브젝트만 생성하고, 해당 오브젝트를 공유하는 싱글톤 패턴을 사용하는 것이다.

 

하지만 기존의 싱글톤 패턴은 약간의 문제점이 있었다. (상속 받으면 기본 생성자가 private이다 등등..)

그렇기에 평범한 자바 클래스를 이런 싱글톤으로 활용할 수 있도록 스프링이 기능을 제공한다.

 

하지만 자바는 멀티 스레드 환경이다.

이러한 상황에서 싱글톤을 사용하게 된다면 인스턴스 변수의 사용에 큰 문제가 발생할 수도 있다.

 

기존의 코드를 다음과 같이 바꿔보자.

package seungkyu;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserDao {

    private final ConnectionHelper connectionHelper;
    
    private Connection c;
    private User user;

    public UserDao(ConnectionHelper connectionHelper) {
        this.connectionHelper = connectionHelper;
    }

    public User get(String id) throws ClassNotFoundException, SQLException {

//        Connection connection = connectionHelper.makeConnection();
        this.c = connectionHelper.makeConnection();
        PreparedStatement preparedStatement = c.prepareStatement(
                "select * from users where id = ?"
        );

        preparedStatement.setString(1, id);

        var resultSet = preparedStatement.executeQuery();

        resultSet.next();

        this.user = new User();
        this.user.setId(resultSet.getString("id"));
        this.user.setName(resultSet.getString("name"));
        this.user.setPassword(resultSet.getString("password"));

        resultSet.close();
        preparedStatement.close();
        c.close();

        return user;
    }
}

 

이러면 UserDao의 User를 공유하게 되기에 해당 메서드를 사용하는 중간에 다른 메서드가 끼어든다면, 내용이 바뀔 수도 있다는 문제가 있다.

 

그렇기에 싱글톤을 사용할 때 다른 싱글톤 빈을 저장하는 용도나 final이라면 인스턴스 변수를 사용해도 괜찮지만, 수정이 될 수 있는 경우에는 인스턴스 변수를 사용하지 않는것이 좋다.

 

 

728x90

스프링에서는 스프링이 제어권을 가지고 직접 만들며 관계를 부여하는 오브젝트를 빈이라고 부른다.

 

자바빈과 비슷한 오브젝트 단위이지만, 스프링 빈은 여기에 스프링 컨테이너가 생성하며 관계설정, 사용등을 제어해주는 제어의 역전이 적용된 오브젝트를 말한다.

 

스프링의 빈 팩토리가 사용할 수 있는 설정정보를 만들어보자.

@Configuration
public class DaoFactory {

    @Bean
    public UserDao userDao(){
        return new UserDao(connectionHelper());
    }

    @Bean
    public ConnectionHelper connectionHelper(){
        return new SeungkyuConnection();
    }
}

 

스프링이 빈 팩토리를 위한 오브젝트 설정을 담당하는 클래스라고 인식할 수 있도록 @Configuration annotation을 달아야 한다.

 

그리고 이 안의 userDao() 메서드는 UserDao 타입 오브젝트를 생성하고 초기화해서 돌려주는 것이기에 @Bean이 붙는다.

다른 부분도 마찬가지로 @Bean을 설정해준다.

 

이 두가지의 annotation 만으로도 애플리케이션 컨텍스트가 IoC 방식을 제공할 수 있게 된다.

 

 

이제 DaoFactory를 설정정보로 사용하는 애플리케이션 컨텍스트를 만들어보자.

ApplicationContext를 구현한 클래스 중에서 Annotation을 사용했기에, AnnotationConfigApplicationContext를 사용했다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);

UserDao userDao = context.getBean("userDao", UserDao.class);

 

해당 방법으로 Configuration을 ApplicationContext에 등록하고 getBean을 사용해 해당 userDao의 이름의 빈을 UserDao.class로 가져오는 것이다.

 

같은 타입의 메서드가 여러개 지정되어 있다면, 이 이름을 통해서 빈을 가져올 수 있다.

 

이렇게 스프링의 ApplicationContext로 기존의 오브젝트 팩토리를 대체했다.

 

ApplicationContext는 DaoFactory 클래스를 설정정보로 등록해두고 @Bean이 붙은 메서드의 이름을 가져와 빈 목록을 만들어둔다.

클라이언트가 애플리케이션 컨텍스트의 getBean() 메서드를 호출하면 자신의 빈 목록에서 요청한 이름이 있는지 찾고, 있다면 빈을 생성하는 메서드를 호출해서 오브젝트를 생성시킨 후 클라이언트에 돌려준다.

 

이렇게 IoC 기능으로 ApplicationContext를 사용하면 다음과 같은 장점이 있다.

 

  • 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없다.

프로젝트가 커져가며 Factory들도 점차 많아질텐데, 그러면 클라이언트가 필요한 오브젝트를 가져오기 위해 Factory 클래스들을 찾아야 한다는 문제가 있다. 이를 ApplicationContext에 등록하면 일관된 방식으로 원하는 오브젝트를 가져올 수 있다.

 

  • ApplicationContext는 종합 IoC 서비스를 제공해준다.

ApplicationContext는 단순하게 오브젝트와 관계설정에만 관여하는 것이 아니다. 오브젝트가 만들어지는 방식, 시점과 전략을 다르게 가져갈 수 있다고 한다. 이를 통해 오브젝트를 효과적으로 활용할 수 있는 다양한 기능을 제공한다.

 

  • 애플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공한다.

애플리케이션 컨텍스트의 getBean() 메서드는 빈의 이름을 이용해 빈을 찾아주기에, 특별한 설정이 되어 있는 빈도 찾을 수 있다.

 

이제 스프링의 IoC 용어를 정리하고 마무리해보겠다.

 

  • 빈(Bean)

빈은 스프링 IoC 방식으로 관리하는 오브젝트라는 뜻이다. 스프링을 사용하는 애플리케이션에서 만들어지는 모든 오브젝트가 빈은 아니다. 그 중에서 스프링이 직접 생성과 제어를 담당하는 오브젝트만을 빈이라고 부른다.

 

  • 빈 팩토리(Bean factory)

스프링의 IoC를 담당하는 핵심 컨테이너를 가리킨다. 빈을 등록하고, 생성하고, 조회하고 돌려주고 그 외의 빈을 관리하는 기능을 담당한다. 하지만 보통은 이를 확장한 ApplicationContext를 사용한다.

 

  • 애플리케이션 컨텍스트(Application Context)

빈 팩토리를 확장한 IoC 컨테이너이다. 기본적인 기능은 빈 팩토리와 동일하며, 여기에 스프링이 제공하는 각종 부가 서비스를 추가로 제공한다. 그렇기에 스프링의 기능을 모두 포함하여 이야기 할 때 Application Context라고 말한다.

 

  • 설정정보(Configuration metadata)

빈 팩토리가 IoC를 적용하기 위해 사용하는 메타정보를 말한다. 컨테이너에 어떤 기능을 세팅하거나 조정하는 경우에도 사용하지만, IoC 컨테이너에 의해 관리되는 애플리케이션 오브젝트를 생성하고 구성할 때도 사용된다.

 

  • 컨테이너

IoC 방식으로 빈을 관리한다는 의미에서 애플리케이션 컨텍스트나 빈 팩토리를 컨테이너라고 한다. 

728x90

책을 읽어봤지만, IOC에 대해 바로 이해할 수는 없어서 자료들을 많이 찾아보고 해당 자료대로 작성해보려 한다.

 

기존에는 개발자가 작성한대로 제어의 흐름이 진행되었다.

이것을 프렘워크가 담당하도록 하는 것을 제어의 역전이라고 한다고 한다.

 

이해가 어려우니 코드로 보도록 하자.

 

만약, UserService를 작성하고 UserServiceImpl로 구현을 해서 개발한다면, 이런 방식으로 UserService를 호출하게 될 것이다.

// 개발자가 직접 객체를 생성하는 방식 (IOC 아님)
UserService userService = new UserServiceImpl();

 

이것에 IOC를 적용한다면 다음과 같다.

// IOC 방식 (스프링이 객체를 대신 생성해주고 주입해줌)
@Autowired
private UserService userService;

 

UserService를 사용은 하지만, 어떤 객체로 사용하는지는 모르고 스프링이 제어해준대로 사용하게 된다.

 

이렇게 생성이나 호출 제어는 프레임워크에게 맡기는 방법을 제어의 역전이라고 한다.

 

스프링에서 굉장히 자주 사용하며, 결합도를 낮춰 확장성과 유지보수에 도움을 준다고 한다.

728x90

지금까지 배운 것을 바탕으로 성능을 개선해보자.

 

동기적으로 작성된 코드를 비동기적으로 변경하는 것이다.

현재 동기적으로 작성된 코드는 다음과 같다.

public Optional<User> getUserById(String id){
        return userRepository.findById(id)
                .map(user -> {
                    var image = imageRepository.findById(user.getProfileImageId())
                            .map(imageEntity -> {
                                return new Image(imageEntity.getId(), imageEntity.getName(), imageEntity.getUrl());
                            });

                    var articles = articleRepository.findAllByUserId(user.getId())
                            .stream().map(articleEntity ->
                                    new Article(articleEntity.getId(), articleEntity.getTitle(), articleEntity.getContent())).toList();

                    var followCount = followRepository.countByUserID(user.getId());

                    return new User(
                            user.getId(),
                            user.getName(),
                            user.getAge(),
                            image,
                            articles,
                            followCount
                    );
                });
    }

 

Repository에서 가져올 때마다 1초 정도 시간이 걸린다고 생각하여 1초 정도 Thread.sleep을 사용했고, 조회한 값들을 다른 객체에 넣어서 반환하는 메서드이다.

 

차례대로 1초씩 3번 호출하기 때문에 적어도 3초 이상의 시간이 소모될 것이다.

 

    void testGetUser(){
        //given
        String userId = "1234";

        //when
        Optional<User> optionalUser = userBlockingService.getUserById(userId);

        //then
        assertFalse(optionalUser.isEmpty());
        var user = optionalUser.get();
        assertEquals(user.getName(), "sk");

        assertFalse(user.getProfileImage().isEmpty());
        assertFalse(user.getProfileImage().isEmpty());
        var image = user.getProfileImage().get();
        assertEquals(image.getId(), "image#1000");
        assertEquals(image.getName(), "profileImage");
        assertEquals(image.getUrl(), "https://avatars.githubusercontent.com/u/98071131?s=400&u=9107a0b50b52da5bbc8528157eed1cca34feb3c5&v=4");

        assertEquals(2, user.getArticleList().size());

        assertEquals(1000, user.getFollowCount());
    }

 

해당 테스트 코드의 시간을 확인 했을 때 4sec 74ms가 소모되었다.

 

이제 변경해보도록 하자.

일단 각 Repository를 CompletableFuture을 반환하도록 메서드를 변경했다.

    @SneakyThrows
    public CompletableFuture<Optional<UserEntity>> findById(String userId){
        return CompletableFuture.supplyAsync(() -> {
            log.info("UserRepository.findById: {}", userId);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            var user = userMap.get(userId);
            return Optional.ofNullable(user);
        });
    }

 

 

현재 에러만 잡아서 변경해놓은 코드는 다음과 같다.

    @SneakyThrows
    public Optional<User> getUserById(String id){
        return userRepository.findById(id).get()
                .map(this::getUser);
    }

    @SneakyThrows
    private User getUser(UserEntity user){
        var image = imageRepository.findById(user.getProfileImageId()).get()
                .map(imageEntity -> {
                    return new Image(imageEntity.getId(), imageEntity.getName(), imageEntity.getUrl());
                });

        var articles = articleRepository.findAllByUserId(user.getId()).get()
                .stream().map(articleEntity ->
                        new Article(articleEntity.getId(), articleEntity.getTitle(), articleEntity.getContent())).toList();

        var followCount = followRepository.countByUserID(user.getId()).get();

        return new User(
                user.getId(),
                user.getName(),
                user.getAge(),
                image,
                articles,
                followCount
        );
    }

 

이거를 Repository에 접근 할 때마다 CompletableFuture를 사용했다.

 

    @SneakyThrows
    public CompletableFuture<Optional<User>> getUserById(String id){
        return userRepository.findById(id)
                .thenCompose(this::getUser);
    }

    @SneakyThrows
    private CompletableFuture<Optional<User>> getUser(Optional<UserEntity> userEntityOptional) {
        if (userEntityOptional.isEmpty()) {
            return CompletableFuture.completedFuture(Optional.empty());
        }

        var userEntity = userEntityOptional.get();

        var imageFuture = imageRepository.findById(userEntity.getProfileImageId())
                .thenApplyAsync(imageEntityOptional ->
                        imageEntityOptional.map(imageEntity ->
                                new Image(imageEntity.getId(), imageEntity.getName(), imageEntity.getUrl())
                        )
                );


        var articlesFuture = articleRepository.findAllByUserId(userEntity.getId())
                .thenApplyAsync(articleEntities ->
                        articleEntities.stream()
                                .map(articleEntity ->
                                        new Article(articleEntity.getId(), articleEntity.getTitle(), articleEntity.getContent())
                                )
                                .collect(Collectors.toList())
                );

        var followCountFuture = followRepository.countByUserID(userEntity.getId());

        return CompletableFuture.allOf(imageFuture, articlesFuture, followCountFuture)
                .thenAcceptAsync(v -> {
                    log.info("Three futures are completed");
                })
                .thenRunAsync(() -> {
                    log.info("Three futures are also completed");
                })
                .thenApplyAsync(v -> {
                    try {
                        var image = imageFuture.get();
                        var articles = articlesFuture.get();
                        var followCount = followCountFuture.get();

                        return Optional.of(
                                new User(
                                        userEntity.getId(),
                                        userEntity.getName(),
                                        userEntity.getAge(),
                                        image,
                                        articles,
                                        followCount
                                )
                        );
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                });
    }
}

 

이렇게 변경하고 전에 사용했던 테스트 코드의 시간을 측정해보았다.

 

거의 반으로 줄어든 2sec 66ms가 나왔다.

 

전 코드에서 Repository에 접근 하는 것을 기다리기 보다 비동기적으로 실행한다면 시간을 크게 줄일 수 있는 것을 볼 수 있었던 것 같다.

'백엔드 > 리액티브 프로그래밍' 카테고리의 다른 글

ColdPublisher 구현  (0) 2024.03.13
Publisher, Subscriber에 대하여  (0) 2024.03.12
CompletableFuture 인터페이스  (1) 2024.03.06
CompletionStage 인터페이스  (1) 2024.03.05
Future 인터페이스  (1) 2024.01.09

+ Recent posts