diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml
index 4e04e179..16b7d8ab 100644
--- a/.github/workflows/build-test.yml
+++ b/.github/workflows/build-test.yml
@@ -26,4 +26,7 @@ jobs:
         run: chmod +x gradlew
 
       - name: Build with Gradle
-        run: ./gradlew build
\ No newline at end of file
+        run: ./gradlew build
+        env:
+          FMP_API_KEY: ${{ secrets.FMP_API_KEY }}
+          NINJAS_API_KEY: ${{ secrets.NINJAS_API_KEY }}
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..eb91add0
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,80 @@
+name: Backend CD
+
+on:
+  push:
+    branches:
+      - main
+      - develop
+      - fix/#77
+
+jobs:
+  build-and-push:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3
+
+      - name: Set up JDK 17
+        uses: actions/setup-java@v3
+        with:
+          java-version: '17'
+          distribution: 'temurin'
+
+      - name: Grant execute permission for gradlew
+        run: chmod +x gradlew
+
+      - name: Execute Gradle build
+        run: ./gradlew clean build
+
+      - name: Set up Docker Build
+        uses: docker/setup-buildx-action@v2
+
+      - name: Docker build and push to NCP container registry and copy docker-compose.yml to server
+        run: |
+          cd ./api-server
+          sudo docker build --build-arg DEPENDENCY=build/dependency -t ${{ secrets.NCP_CONTAINER_REGISTRY_API }}/payout-api --platform linux/amd64 .
+          sudo docker login ${{ secrets.NCP_CONTAINER_REGISTRY_API }} -u ${{ secrets.NCP_ACCESS_KEY }} -p ${{ secrets.NCP_SECRET_KEY }}
+          sudo docker push ${{ secrets.NCP_CONTAINER_REGISTRY_API }}/payout-api
+          
+          cd ../batch
+          sudo docker build --build-arg DEPENDENCY=build/dependency -t ${{ secrets.NCP_CONTAINER_REGISTRY_BATCH }}/payout-batch --platform linux/amd64 .
+          sudo docker login ${{ secrets.NCP_CONTAINER_REGISTRY_BATCH }} -u ${{ secrets.NCP_ACCESS_KEY }} -p ${{ secrets.NCP_SECRET_KEY }}
+          sudo docker push ${{ secrets.NCP_CONTAINER_REGISTRY_BATCH }}/payout-batch
+          
+          cd ..
+          sshpass -p ${{ secrets.API_SERVER_PASSWORD }} scp -o StrictHostKeyChecking=no ./docker-compose.yml ${{ secrets.API_SERVER_USERNAME }}@${{ secrets.API_SERVER_HOST }}:${{ secrets.DOCKER_COMPOSE_PATH }}
+          sshpass -p ${{ secrets.API_SERVER_PASSWORD }} scp -o StrictHostKeyChecking=no ./nginx/nginx.conf ${{ secrets.API_SERVER_USERNAME }}@${{ secrets.API_SERVER_HOST }}:${{ secrets.DOCKER_COMPOSE_PATH }}
+          sshpass -p ${{ secrets.API_SERVER_PASSWORD }} scp -o StrictHostKeyChecking=no ./deploy.sh ${{ secrets.API_SERVER_USERNAME }}@${{ secrets.API_SERVER_HOST }}:${{ secrets.DOCKER_COMPOSE_PATH }}
+        shell: bash
+
+  deploy-to-server:
+    name: Connect api server ssh and pull from container registry
+    needs: build-and-push
+    runs-on: ubuntu-latest
+    steps:
+      ## docker compose up
+      - name: Deploy to api server
+        uses: appleboy/ssh-action@master
+        with:
+          host: ${{ secrets.API_SERVER_HOST }}
+          username: ${{ secrets.API_SERVER_USERNAME }}
+          password: ${{ secrets.API_SERVER_PASSWORD }}
+          script: |
+            export DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}
+            export DB_PORT=${{ secrets.DB_PORT }}
+            export DB_DATABASE=${{ secrets.DB_DATABASE }}
+            export DB_USERNAME=${{ secrets.DB_USERNAME }}
+            export DB_PASSWORD=${{ secrets.DB_PASSWORD }}
+            export FMP_API_KEY=${{ secrets.FMP_API_KEY }}
+            export NINJAS_API_KEY=${{ secrets.NINJAS_API_KEY }}
+            export NCP_CONTAINER_REGISTRY_API=${{ secrets.NCP_CONTAINER_REGISTRY_API }}
+            export NCP_CONTAINER_REGISTRY_BATCH=${{ secrets.NCP_CONTAINER_REGISTRY_BATCH }}
+            
+            sudo docker login ${{ secrets.NCP_CONTAINER_REGISTRY_API }} -u ${{ secrets.NCP_ACCESS_KEY }} -p ${{ secrets.NCP_SECRET_KEY }}
+            sudo docker pull ${{ secrets.NCP_CONTAINER_REGISTRY_API }}/payout-api
+            sudo docker login ${{ secrets.NCP_CONTAINER_REGISTRY_BATCH }} -u ${{ secrets.NCP_ACCESS_KEY }} -p ${{ secrets.NCP_SECRET_KEY }}
+            sudo docker pull ${{ secrets.NCP_CONTAINER_REGISTRY_BATCH }}/payout-batch
+            
+            bash ${{ secrets.DOCKER_COMPOSE_PATH }}/deploy.sh
+
+            docker image prune -f
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 2eb1fafa..10458750 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,4 +37,6 @@ out/
 .vscode/
 .DS_Store
 
-**/logs
\ No newline at end of file
+**/logs
+**/db/data
+domain/src/main/generated/nexters/payout/domain/stock/domain/QStock.java
diff --git a/api-server/Dockerfile b/api-server/Dockerfile
new file mode 100644
index 00000000..6e14fe64
--- /dev/null
+++ b/api-server/Dockerfile
@@ -0,0 +1,5 @@
+FROM eclipse-temurin:17
+
+ARG JAR_FILE=build/libs/api-server.jar
+COPY ${JAR_FILE} api-server.jar
+ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod,file-logging","-Duser.timezone=UTC","/api-server.jar"]
\ No newline at end of file
diff --git a/api-server/build.gradle b/api-server/build.gradle
index 632f4fcb..a640cb42 100644
--- a/api-server/build.gradle
+++ b/api-server/build.gradle
@@ -6,9 +6,20 @@ plugins {
 
 dependencies {
     implementation(project(":core"))
+    implementation(project(":domain"))
+
+    compileOnly 'org.projectlombok:lombok'
+    annotationProcessor 'org.projectlombok:lombok'
 
     implementation 'org.springframework.boot:spring-boot-starter-web'
+    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+    implementation 'org.springframework.boot:spring-boot-starter-validation'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    testImplementation 'io.rest-assured:rest-assured:5.3.0'
+
+    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
+
+    testImplementation(testFixtures(project(":domain")))
 }
 
 tasks.named('test') {
diff --git a/api-server/src/main/java/nexters/dividend/apiserver/DividendApiServerApplication.java b/api-server/src/main/java/nexters/dividend/apiserver/DividendApiServerApplication.java
deleted file mode 100644
index d66a9812..00000000
--- a/api-server/src/main/java/nexters/dividend/apiserver/DividendApiServerApplication.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package nexters.dividend.apiserver;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class DividendApiServerApplication {
-
-	public static void main(String[] args) {
-		SpringApplication.run(DividendApiServerApplication.class, args);
-	}
-
-}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/PayoutApiServerApplication.java b/api-server/src/main/java/nexters/payout/apiserver/PayoutApiServerApplication.java
new file mode 100644
index 00000000..bc042a32
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/PayoutApiServerApplication.java
@@ -0,0 +1,19 @@
+package nexters.payout.apiserver;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+
+@ConfigurationPropertiesScan
+@SpringBootApplication(scanBasePackages = {
+		"nexters.payout.core",
+		"nexters.payout.domain",
+		"nexters.payout.apiserver"
+})
+public class PayoutApiServerApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(PayoutApiServerApplication.class, args);
+	}
+
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/config/SwaggerConfig.java b/api-server/src/main/java/nexters/payout/apiserver/config/SwaggerConfig.java
new file mode 100644
index 00000000..40f8b500
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/config/SwaggerConfig.java
@@ -0,0 +1,26 @@
+package nexters.payout.apiserver.config;
+
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.servers.Server;
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SwaggerConfig {
+
+    @Bean
+    public OpenAPI openAPI() {
+        return new OpenAPI()
+                .components(new Components())
+                .addServersItem(new Server().url("/"))
+                .info(getPayoutServerInfo());
+    }
+
+    private Info getPayoutServerInfo() {
+        return new Info().title("Payout Server API")
+                .description("Payout Server API 명세서입니다.")
+                .version("1.0.0");
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/config/WebConfig.java b/api-server/src/main/java/nexters/payout/apiserver/config/WebConfig.java
new file mode 100644
index 00000000..be3d3ded
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/config/WebConfig.java
@@ -0,0 +1,19 @@
+package nexters.payout.apiserver.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("http://localhost:3000", "http://localhost:8080")
+                .allowedOriginPatterns("*")
+                .allowedMethods("*")
+                .allowedHeaders("*")
+                .allowCredentials(true);
+    }
+}
\ No newline at end of file
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/DividendQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/DividendQueryService.java
new file mode 100644
index 00000000..a9f6db6f
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/DividendQueryService.java
@@ -0,0 +1,98 @@
+package nexters.payout.apiserver.dividend.application;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nexters.payout.apiserver.dividend.application.dto.request.DividendRequest;
+import nexters.payout.apiserver.dividend.application.dto.request.TickerShare;
+import nexters.payout.apiserver.dividend.application.dto.response.SingleMonthlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.MonthlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.SingleYearlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.YearlyDividendResponse;
+import nexters.payout.core.exception.error.NotFoundException;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@Transactional(readOnly = true)
+public class DividendQueryService {
+
+    private final DividendRepository dividendRepository;
+    private final StockRepository stockRepository;
+
+    public List<MonthlyDividendResponse> getMonthlyDividends(final DividendRequest request) {
+        return InstantProvider.generateNext12Months()
+                .stream()
+                .map(yearMonth -> MonthlyDividendResponse.of(
+                                yearMonth.getYear(),
+                                yearMonth.getMonthValue(),
+                                getDividendsOfLastYearAndMonth(request.tickerShares(), yearMonth.getMonthValue())
+                        )
+                )
+                .collect(Collectors.toList());
+    }
+
+    public YearlyDividendResponse getYearlyDividends(final DividendRequest request) {
+        List<SingleYearlyDividendResponse> dividends = request.tickerShares()
+                .stream()
+                .map(tickerShare -> {
+                    String ticker = tickerShare.ticker();
+                    return SingleYearlyDividendResponse.of(
+                            getStock(ticker), tickerShare.share(), getYearlyDividend(ticker)
+                    );
+                })
+                .filter(response -> response.totalDividend() != 0)
+                .collect(Collectors.toList());
+
+        return YearlyDividendResponse.of(dividends);
+    }
+
+    private double getYearlyDividend(final String ticker) {
+        return getLastYearDividendsByTicker(ticker)
+                .stream()
+                .mapToDouble(Dividend::getDividend)
+                .sum();
+    }
+
+    private List<Dividend> getLastYearDividendsByTicker(final String ticker) {
+        return dividendRepository.findAllByTickerAndYear(ticker, InstantProvider.getLastYear());
+    }
+
+    private Stock getStock(final String ticker) {
+        return stockRepository.findByTicker(ticker)
+                .orElseThrow(() -> new NotFoundException(String.format("not found ticker [%s]", ticker)));
+    }
+
+    private List<SingleMonthlyDividendResponse> getDividendsOfLastYearAndMonth(
+            final List<TickerShare> tickerShares, final int month
+    ) {
+        return tickerShares
+                .stream()
+                .flatMap(tickerShare -> stockRepository.findByTicker(tickerShare.ticker())
+                        .map(stock -> getMonthlyDividendResponse(month, tickerShare, stock))
+                        .orElseThrow(() -> new NotFoundException(String.format("not found ticker [%s]", tickerShare.ticker()))))
+                .toList();
+    }
+
+    private Stream<SingleMonthlyDividendResponse> getMonthlyDividendResponse(
+            final int month, final TickerShare tickerShare, final Stock stock
+    ) {
+        return getLastYearDividendsByTickerAndMonth(tickerShare.ticker(), month)
+                .stream()
+                .map(dividend -> SingleMonthlyDividendResponse.of(stock, tickerShare.share(), dividend));
+    }
+
+    private List<Dividend> getLastYearDividendsByTickerAndMonth(final String ticker, final int month) {
+        return dividendRepository.findAllByTickerAndYearAndMonth(ticker, InstantProvider.getLastYear(), month);
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/request/DividendRequest.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/request/DividendRequest.java
new file mode 100644
index 00000000..8d616bd6
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/request/DividendRequest.java
@@ -0,0 +1,15 @@
+package nexters.payout.apiserver.dividend.application.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Size;
+
+import java.util.List;
+
+public record DividendRequest(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        @Valid
+        @Size(min = 1)
+        List<TickerShare> tickerShares
+) {
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/request/TickerShare.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/request/TickerShare.java
new file mode 100644
index 00000000..5011497a
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/request/TickerShare.java
@@ -0,0 +1,14 @@
+package nexters.payout.apiserver.dividend.application.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotEmpty;
+
+public record TickerShare(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "ticker name")
+        @NotEmpty
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "share")
+        @Min(value = 1)
+        Integer share
+) { }
\ No newline at end of file
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/MonthlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/MonthlyDividendResponse.java
new file mode 100644
index 00000000..74868181
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/MonthlyDividendResponse.java
@@ -0,0 +1,32 @@
+package nexters.payout.apiserver.dividend.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Comparator;
+import java.util.List;
+
+public record MonthlyDividendResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer year,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer month,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        List<SingleMonthlyDividendResponse> dividends,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double totalDividend
+) {
+    public static MonthlyDividendResponse of(
+            final int year, final int month, final List<SingleMonthlyDividendResponse> dividends
+    ) {
+        return new MonthlyDividendResponse(
+                year,
+                month,
+                dividends.stream()
+                        .sorted(Comparator.comparingDouble(SingleMonthlyDividendResponse::totalDividend).reversed())
+                        .toList(),
+                dividends.stream()
+                        .mapToDouble(SingleMonthlyDividendResponse::totalDividend)
+                        .sum()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/SingleMonthlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/SingleMonthlyDividendResponse.java
new file mode 100644
index 00000000..76be8ba0
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/SingleMonthlyDividendResponse.java
@@ -0,0 +1,28 @@
+package nexters.payout.apiserver.dividend.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+
+public record SingleMonthlyDividendResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String logoUrl,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer share,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double dividend,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double totalDividend
+) {
+    public static SingleMonthlyDividendResponse of(Stock stock, int share, Dividend dividend) {
+        return new SingleMonthlyDividendResponse(
+                stock.getTicker(),
+                stock.getLogoUrl(),
+                share,
+                dividend.getDividend(),
+                dividend.getDividend() * share
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/SingleYearlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/SingleYearlyDividendResponse.java
new file mode 100644
index 00000000..966ee592
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/SingleYearlyDividendResponse.java
@@ -0,0 +1,24 @@
+package nexters.payout.apiserver.dividend.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.stock.domain.Stock;
+
+public record SingleYearlyDividendResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String logoUrl,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer share,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double totalDividend
+) {
+    public static SingleYearlyDividendResponse of(Stock stock, int share, double dividend) {
+        return new SingleYearlyDividendResponse(
+                stock.getTicker(),
+                stock.getLogoUrl(),
+                share,
+                dividend * share
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/YearlyDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/YearlyDividendResponse.java
new file mode 100644
index 00000000..0fe15be8
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/application/dto/response/YearlyDividendResponse.java
@@ -0,0 +1,28 @@
+package nexters.payout.apiserver.dividend.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.util.Comparator;
+import java.util.List;
+
+public record YearlyDividendResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        List<SingleYearlyDividendResponse> dividends,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double totalDividend
+) {
+    public static YearlyDividendResponse of(List<SingleYearlyDividendResponse> dividends) {
+
+        dividends = dividends
+                .stream()
+                .sorted(Comparator.comparingDouble(SingleYearlyDividendResponse::totalDividend).reversed())
+                .toList();
+        return new YearlyDividendResponse(
+                dividends,
+                dividends
+                        .stream()
+                        .mapToDouble(SingleYearlyDividendResponse::totalDividend)
+                        .sum()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/presentation/DividendController.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/presentation/DividendController.java
new file mode 100644
index 00000000..6b01b90d
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/presentation/DividendController.java
@@ -0,0 +1,39 @@
+package nexters.payout.apiserver.dividend.presentation;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nexters.payout.apiserver.dividend.application.DividendQueryService;
+import nexters.payout.apiserver.dividend.application.dto.request.DividendRequest;
+import nexters.payout.apiserver.dividend.application.dto.response.MonthlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.YearlyDividendResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping("/api/dividends")
+public class DividendController implements DividendControllerDocs {
+
+    private final DividendQueryService dividendQueryService;
+
+    @PostMapping("/monthly")
+    public ResponseEntity<List<MonthlyDividendResponse>> getMonthlyDividends(
+            @RequestBody @Valid final DividendRequest request
+    ) {
+        return ResponseEntity.ok(dividendQueryService.getMonthlyDividends(request));
+    }
+
+    @PostMapping("/yearly")
+    public ResponseEntity<YearlyDividendResponse> getYearlyDividends(
+            @RequestBody @Valid final DividendRequest request
+    ) {
+        return ResponseEntity.ok(dividendQueryService.getYearlyDividends(request));
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/dividend/presentation/DividendControllerDocs.java b/api-server/src/main/java/nexters/payout/apiserver/dividend/presentation/DividendControllerDocs.java
new file mode 100644
index 00000000..f6b0fa01
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/dividend/presentation/DividendControllerDocs.java
@@ -0,0 +1,60 @@
+package nexters.payout.apiserver.dividend.presentation;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.validation.Valid;
+import nexters.payout.apiserver.dividend.application.dto.request.DividendRequest;
+import nexters.payout.apiserver.dividend.application.dto.response.MonthlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.YearlyDividendResponse;
+import nexters.payout.core.exception.ErrorResponse;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.RequestBody;
+
+import java.util.List;
+
+public interface DividendControllerDocs {
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "404", description = "NOT FOUND",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "월별 배당금 조회",
+            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    required = true,
+                    content = @Content(mediaType = "application/json",
+                            schema = @Schema(implementation = DividendRequest.class),
+                            examples = {
+                                    @ExampleObject(name = "DividendRequestExample", value = "{\"tickerShares\":[{\"ticker\":\"AAPL\",\"share\":3}]}")
+                            })))
+    ResponseEntity<List<MonthlyDividendResponse>> getMonthlyDividends(@RequestBody @Valid DividendRequest request);
+
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "404", description = "NOT FOUND",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "연간 배당금 조회",
+            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    required = true,
+                    content = @Content(mediaType = "application/json",
+                            schema = @Schema(implementation = DividendRequest.class),
+                            examples = {
+                                    @ExampleObject(name = "DividendRequestExample", value = "{\"tickerShares\":[{\"ticker\":\"AAPL\",\"share\":3}]}")
+                            })))
+    ResponseEntity<YearlyDividendResponse> getYearlyDividends(@RequestBody @Valid DividendRequest request);
+}
+
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/StockQueryService.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/StockQueryService.java
new file mode 100644
index 00000000..d2e4ac8a
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/StockQueryService.java
@@ -0,0 +1,155 @@
+package nexters.payout.apiserver.stock.application;
+
+import lombok.RequiredArgsConstructor;
+import nexters.payout.apiserver.stock.application.dto.request.SectorRatioRequest;
+import nexters.payout.apiserver.stock.application.dto.request.TickerShare;
+import nexters.payout.apiserver.stock.application.dto.response.*;
+import nexters.payout.core.exception.error.NotFoundException;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import nexters.payout.domain.stock.domain.service.StockDividendAnalysisService;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService.StockShare;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Month;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class StockQueryService {
+
+    private final StockRepository stockRepository;
+    private final DividendRepository dividendRepository;
+    private final SectorAnalysisService sectorAnalysisService;
+    private final StockDividendAnalysisService dividendAnalysisService;
+
+    public List<StockResponse> searchStock(final String keyword, final Integer pageNumber, final Integer pageSize) {
+        return stockRepository.findStocksByTickerOrNameWithPriority(keyword, pageNumber, pageSize)
+                .stream()
+                .map(StockResponse::from)
+                .collect(Collectors.toList());
+    }
+
+    public StockDetailResponse getStockByTicker(final String ticker) {
+        Stock stock = getStock(ticker);
+
+        List<Dividend> lastYearDividends = getLastYearDividends(stock);
+        List<Dividend> thisYearDividends = getThisYearDividends(stock);
+
+        if (lastYearDividends.isEmpty() && thisYearDividends.isEmpty()) {
+            return StockDetailResponse.of(stock, DividendResponse.noDividend());
+        }
+
+        List<Month> dividendMonths = dividendAnalysisService.calculateDividendMonths(stock, lastYearDividends);
+        Double dividendYield = dividendAnalysisService.calculateDividendYield(stock, lastYearDividends);
+        Double dividendPerShare = dividendAnalysisService.calculateAverageDividend(
+                combinedDividends(lastYearDividends, thisYearDividends)
+        );
+
+        return dividendAnalysisService.findUpcomingDividend(lastYearDividends, thisYearDividends)
+                .map(upcomingDividend -> StockDetailResponse.of(
+                        stock,
+                        DividendResponse.fullDividendInfo(upcomingDividend, dividendYield, dividendMonths)
+                ))
+                .orElse(StockDetailResponse.of(
+                        stock,
+                        DividendResponse.withoutDividendDates(dividendPerShare, dividendYield, dividendMonths)
+                ));
+    }
+
+    private List<Dividend> combinedDividends(final List<Dividend> lastYearDividends, final List<Dividend> thisYearDividends) {
+        return Stream.of(lastYearDividends, thisYearDividends)
+                .flatMap(List::stream)
+                .collect(Collectors.toList());
+    }
+
+    private Stock getStock(final String ticker) {
+        return stockRepository.findByTicker(ticker)
+                .orElseThrow(() -> new NotFoundException(String.format("not found ticker [%s]", ticker)));
+    }
+
+    private List<Dividend> getLastYearDividends(final Stock stock) {
+        int lastYear = InstantProvider.getLastYear();
+
+        return dividendRepository.findAllByStockId(stock.getId())
+                .stream()
+                .filter(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()).getYear() == lastYear)
+                .collect(Collectors.toList());
+    }
+
+    private List<Dividend> getThisYearDividends(final Stock stock) {
+        int thisYear = InstantProvider.getThisYear();
+
+        return dividendRepository.findAllByStockId(stock.getId())
+                .stream()
+                .filter(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()).getYear() == thisYear)
+                .collect(Collectors.toList());
+    }
+
+    public List<SectorRatioResponse> analyzeSectorRatio(final SectorRatioRequest request) {
+        List<StockShare> stockShares = getStockShares(request);
+
+        Map<Sector, SectorInfo> sectorInfoMap = sectorAnalysisService.calculateSectorRatios(stockShares);
+
+        return SectorRatioResponse.fromMap(sectorInfoMap);
+    }
+
+    public UpcomingDividendResponse getUpcomingDividendStocks(final Sector sector, final int pageNumber, final int pageSize) {
+        return UpcomingDividendResponse.of(
+                stockRepository.findUpcomingDividendStock(sector, pageNumber, pageSize)
+                        .stream()
+                        .map(stockDividend -> SingleUpcomingDividendResponse.of(
+                                stockDividend.stock(),
+                                stockDividend.dividend())
+                        )
+                        .collect(Collectors.toList())
+        );
+    }
+
+    public StockDividendYieldResponse getBiggestDividendStocks(final Sector sector, final int pageNumber, final int pageSize) {
+        return StockDividendYieldResponse.of(
+                stockRepository.findBiggestDividendYieldStock(InstantProvider.getLastYear(), sector, pageNumber, pageSize)
+                        .stream()
+                        .map(stockDividendYield -> SingleStockDividendYieldResponse.of(
+                                stockDividendYield.stock(),
+                                stockDividendYield.dividendYield())
+                        )
+                        .collect(Collectors.toList())
+        );
+    }
+
+    private List<StockShare> getStockShares(final SectorRatioRequest request) {
+        List<Stock> stocks = stockRepository.findAllByTickerIn(getTickers(request));
+
+        return stocks
+                .stream()
+                .map(stock -> new StockShare(
+                        stock,
+                        getTickerShareMap(request).get(stock.getTicker())))
+                .collect(Collectors.toList());
+    }
+
+    private List<String> getTickers(final SectorRatioRequest request) {
+        return request.tickerShares()
+                .stream()
+                .map(TickerShare::ticker)
+                .collect(Collectors.toList());
+    }
+
+    private Map<String, Integer> getTickerShareMap(final SectorRatioRequest request) {
+        return request.tickerShares()
+                .stream()
+                .collect(Collectors.toMap(TickerShare::ticker, TickerShare::share));
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/request/SectorRatioRequest.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/request/SectorRatioRequest.java
new file mode 100644
index 00000000..887c6896
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/request/SectorRatioRequest.java
@@ -0,0 +1,16 @@
+package nexters.payout.apiserver.stock.application.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.Size;
+
+import java.util.List;
+
+public record SectorRatioRequest(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        @Valid
+        @Size(min = 1)
+        List<TickerShare> tickerShares
+) {
+}
+
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/request/TickerShare.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/request/TickerShare.java
new file mode 100644
index 00000000..1d975bb3
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/request/TickerShare.java
@@ -0,0 +1,16 @@
+package nexters.payout.apiserver.stock.application.dto.request;
+
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotEmpty;
+
+public record TickerShare(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        @NotEmpty
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        @Min(value = 1)
+        Integer share
+) {
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/DividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/DividendResponse.java
new file mode 100644
index 00000000..586b134f
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/DividendResponse.java
@@ -0,0 +1,57 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.dividend.domain.Dividend;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.Collections;
+import java.util.List;
+
+public record DividendResponse(
+        Double dividendPerShare,
+        LocalDate upcomingExDividendDate,
+        LocalDate paymentDate,
+        Double dividendYield,
+        List<Month> dividendMonths
+) {
+
+    public static DividendResponse noDividend() {
+        return new DividendResponse(
+                0.0,
+                null,
+                null,
+                0.0,
+                Collections.emptyList()
+        );
+    }
+
+    public static DividendResponse withoutDividendDates(
+            final Double dividendPerShare,
+            final Double dividendYield,
+            final List<Month> dividendMonths
+    ) {
+        return new DividendResponse(
+                dividendPerShare,
+                null,
+                null,
+                dividendYield,
+                dividendMonths
+        );
+    }
+
+    public static DividendResponse fullDividendInfo(
+            final Dividend dividend,
+            final Double dividendYield,
+            final List<Month> dividendMonths
+    ) {
+        return new DividendResponse(
+                dividend.getDividend(),
+                InstantProvider.toLocalDate(dividend.getExDividendDate()).withYear(InstantProvider.getThisYear()),
+                dividend.getPaymentDate() == null ? null : InstantProvider.toLocalDate(dividend.getPaymentDate())
+                        .withYear(InstantProvider.getThisYear()),
+                dividendYield,
+                dividendMonths
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SectorRatioResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SectorRatioResponse.java
new file mode 100644
index 00000000..ac3226d4
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SectorRatioResponse.java
@@ -0,0 +1,36 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public record SectorRatioResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector name")
+        String sectorName,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector value")
+        String sectorValue,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED, example = "sector ratio")
+        Double sectorRatio,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        List<StockShareResponse> stockShares
+) {
+    public static List<SectorRatioResponse> fromMap(final Map<Sector, SectorInfo> sectorRatioMap) {
+        return sectorRatioMap.entrySet()
+                .stream()
+                .map(entry -> new SectorRatioResponse(
+                        entry.getKey().getName(),
+                        entry.getKey().name(),
+                        entry.getValue().ratio(),
+                        entry.getValue()
+                                .stockShares()
+                                .stream()
+                                .map(StockShareResponse::from)
+                                .collect(Collectors.toList()))
+                )
+                .collect(Collectors.toList());
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SingleStockDividendYieldResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SingleStockDividendYieldResponse.java
new file mode 100644
index 00000000..dcbf7fc3
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SingleStockDividendYieldResponse.java
@@ -0,0 +1,32 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.time.Instant;
+import java.util.UUID;
+
+public record SingleStockDividendYieldResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        UUID stockId,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String logoUrl,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double dividendYield,
+        @JsonIgnore
+        Instant lastModifiedAt
+) {
+
+    public static SingleStockDividendYieldResponse of(final Stock stock, final Double dividendYield) {
+        return new SingleStockDividendYieldResponse(
+                stock.getId(),
+                stock.getTicker(),
+                stock.getLogoUrl(),
+                dividendYield,
+                stock.getLastModifiedAt()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SingleUpcomingDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SingleUpcomingDividendResponse.java
new file mode 100644
index 00000000..bdcf50f9
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/SingleUpcomingDividendResponse.java
@@ -0,0 +1,32 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.time.Instant;
+import java.util.UUID;
+
+public record SingleUpcomingDividendResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        UUID stockId,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String logoUrl,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Instant exDividendDate,
+        @JsonIgnore
+        Instant lastModifiedAt
+) {
+    public static SingleUpcomingDividendResponse of(final Stock stock, final Dividend dividend) {
+        return new SingleUpcomingDividendResponse(
+                stock.getId(),
+                stock.getTicker(),
+                stock.getLogoUrl(),
+                dividend.getExDividendDate(),
+                dividend.getLastModifiedAt()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockDetailResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockDetailResponse.java
new file mode 100644
index 00000000..8e9a3b44
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockDetailResponse.java
@@ -0,0 +1,97 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.List;
+import java.util.UUID;
+
+public record StockDetailResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        UUID stockId,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String companyName,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String sectorName,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String sectorValue,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String exchange,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String industry,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double price,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer volume,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String logoUrl,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double dividendPerShare,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        LocalDate exDividendDate,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        LocalDate earliestPaymentDate,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double dividendYield,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        List<Month> dividendMonths,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Instant lastModifiedAt
+) {
+
+    public static StockDetailResponse of(
+            final Stock stock, final DividendResponse dividendResponse
+    ) {
+        return new StockDetailResponse(
+                stock.getId(),
+                stock.getTicker(),
+                stock.getName(),
+                stock.getSector().getName(),
+                stock.getSector().name(),
+                stock.getExchange(),
+                stock.getIndustry(),
+                stock.getPrice(),
+                stock.getVolume(),
+                stock.getLogoUrl(),
+                dividendResponse.dividendPerShare(),
+                dividendResponse.upcomingExDividendDate(),
+                dividendResponse.paymentDate(),
+                dividendResponse.dividendYield(),
+                dividendResponse.dividendMonths(),
+                stock.getLastModifiedAt()
+        );
+    }
+
+    public static StockDetailResponse of(
+            final Stock stock, final Dividend dividend, final List<Month> dividendMonths, final Double dividendYield
+    ) {
+        int thisYear = InstantProvider.getThisYear();
+        return new StockDetailResponse(
+                stock.getId(),
+                stock.getTicker(),
+                stock.getName(),
+                stock.getSector().getName(),
+                stock.getSector().name(),
+                stock.getExchange(),
+                stock.getIndustry(),
+                stock.getPrice(),
+                stock.getVolume(),
+                stock.getLogoUrl(),
+                dividend.getDividend(),
+                InstantProvider.toLocalDate(dividend.getExDividendDate()).withYear(thisYear),
+                dividend.getPaymentDate() == null ? null :
+                        InstantProvider.toLocalDate(dividend.getPaymentDate()).withYear(thisYear),
+                dividendYield,
+                dividendMonths,
+                stock.getLastModifiedAt()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockDividendYieldResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockDividendYieldResponse.java
new file mode 100644
index 00000000..54ee8836
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockDividendYieldResponse.java
@@ -0,0 +1,18 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.Instant;
+import java.util.List;
+
+public record StockDividendYieldResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        List<SingleStockDividendYieldResponse> dividends,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Instant lastModifiedAt
+) {
+    public static StockDividendYieldResponse of(List<SingleStockDividendYieldResponse> dividends) {
+        return dividends.isEmpty() ? new StockDividendYieldResponse(dividends, null) :
+                new StockDividendYieldResponse(dividends, dividends.get(0).lastModifiedAt());
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockResponse.java
new file mode 100644
index 00000000..e4b1c62d
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockResponse.java
@@ -0,0 +1,44 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.util.UUID;
+
+public record StockResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        UUID stockId,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String ticker,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String companyName,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String sectorName,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String sectorValue,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String exchange,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String industry,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Double price,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer volume,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        String logoUrl
+) {
+    public static StockResponse from(final Stock stock) {
+        return new StockResponse(
+                stock.getId(),
+                stock.getTicker(),
+                stock.getName(),
+                stock.getSector().getName(),
+                stock.getSector().name(),
+                stock.getExchange(),
+                stock.getIndustry(),
+                stock.getPrice(),
+                stock.getVolume(),
+                stock.getLogoUrl()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockShareResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockShareResponse.java
new file mode 100644
index 00000000..73a52eb5
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/StockShareResponse.java
@@ -0,0 +1,19 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService.StockShare;
+
+public record StockShareResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        StockResponse stockResponse,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Integer share
+) {
+
+    public static StockShareResponse from(final StockShare stockShare) {
+        return new StockShareResponse(
+                StockResponse.from(stockShare.stock()),
+                stockShare.share()
+        );
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/UpcomingDividendResponse.java b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/UpcomingDividendResponse.java
new file mode 100644
index 00000000..cd594d62
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/application/dto/response/UpcomingDividendResponse.java
@@ -0,0 +1,18 @@
+package nexters.payout.apiserver.stock.application.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.Instant;
+import java.util.List;
+
+public record UpcomingDividendResponse(
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        List<SingleUpcomingDividendResponse> dividends,
+        @Schema(requiredMode = Schema.RequiredMode.REQUIRED)
+        Instant lastModifiedAt
+) {
+    public static UpcomingDividendResponse of(List<SingleUpcomingDividendResponse> dividends) {
+        return dividends.isEmpty() ? new UpcomingDividendResponse(dividends, null) :
+                new UpcomingDividendResponse(dividends, dividends.get(0).lastModifiedAt());
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/presentation/StockController.java b/api-server/src/main/java/nexters/payout/apiserver/stock/presentation/StockController.java
new file mode 100644
index 00000000..d1b1c0af
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/presentation/StockController.java
@@ -0,0 +1,64 @@
+package nexters.payout.apiserver.stock.presentation;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import nexters.payout.apiserver.stock.application.StockQueryService;
+import nexters.payout.apiserver.stock.application.dto.request.SectorRatioRequest;
+import nexters.payout.apiserver.stock.application.dto.response.*;
+import nexters.payout.domain.stock.domain.Sector;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/stocks")
+public class StockController implements StockControllerDocs {
+
+    private final StockQueryService stockQueryService;
+
+    @GetMapping("/search")
+    public ResponseEntity<List<StockResponse>> searchStock(
+            @RequestParam @NotEmpty final String keyword,
+            @RequestParam @NotNull final Integer pageNumber,
+            @RequestParam @NotNull final Integer pageSize
+    ) {
+        return ResponseEntity.ok(stockQueryService.searchStock(keyword, pageNumber, pageSize));
+    }
+
+    @GetMapping("/{ticker}")
+    public ResponseEntity<StockDetailResponse> getStockByTicker(
+            @PathVariable final String ticker
+    ) {
+        return ResponseEntity.ok(stockQueryService.getStockByTicker(ticker));
+    }
+
+
+    @PostMapping("/sector-ratio")
+    public ResponseEntity<List<SectorRatioResponse>> findSectorRatios(
+            @Valid @RequestBody final SectorRatioRequest request
+    ) {
+        return ResponseEntity.ok(stockQueryService.analyzeSectorRatio(request));
+    }
+
+    @GetMapping("/ex-dividend-dates/upcoming")
+    public ResponseEntity<UpcomingDividendResponse> getUpComingDividendStocks(
+            @RequestParam @NotNull final Sector sector,
+            @RequestParam @NotNull final Integer pageNumber,
+            @RequestParam @NotNull final Integer pageSize
+    ) {
+        return ResponseEntity.ok(stockQueryService.getUpcomingDividendStocks(sector, pageNumber, pageSize));
+    }
+
+    @GetMapping("/dividend-yields/highest")
+    public ResponseEntity<StockDividendYieldResponse> getBiggestDividendYieldStocks(
+            @RequestParam @NotNull final Sector sector,
+            @RequestParam @NotNull final Integer pageNumber,
+            @RequestParam @NotNull final Integer pageSize
+    ) {
+        return ResponseEntity.ok(stockQueryService.getBiggestDividendStocks(sector, pageNumber, pageSize));
+    }
+}
diff --git a/api-server/src/main/java/nexters/payout/apiserver/stock/presentation/StockControllerDocs.java b/api-server/src/main/java/nexters/payout/apiserver/stock/presentation/StockControllerDocs.java
new file mode 100644
index 00000000..d03ac1b2
--- /dev/null
+++ b/api-server/src/main/java/nexters/payout/apiserver/stock/presentation/StockControllerDocs.java
@@ -0,0 +1,113 @@
+package nexters.payout.apiserver.stock.presentation;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.ExampleObject;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import nexters.payout.apiserver.dividend.application.dto.request.DividendRequest;
+import nexters.payout.apiserver.stock.application.dto.request.SectorRatioRequest;
+import nexters.payout.apiserver.stock.application.dto.response.*;
+import nexters.payout.core.exception.ErrorResponse;
+import nexters.payout.domain.stock.domain.Sector;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.List;
+
+public interface StockControllerDocs {
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "티커명/회사명 검색")
+    ResponseEntity<List<StockResponse>> searchStock(
+            @Parameter(description = "ticker name or company name of stock ex) APPL, APPLE", required = true)
+            @RequestParam @NotEmpty String ticker,
+            @Parameter(description = "page number(start with 1) for pagination", example = "1", required = true)
+            @RequestParam @NotNull final Integer pageNumber,
+            @Parameter(description = "page size for pagination", example = "20", required = true)
+            @RequestParam @NotNull final Integer pageSize
+    );
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "404", description = "NOT FOUND",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "종목 상세 조회")
+    ResponseEntity<StockDetailResponse> getStockByTicker(
+            @Parameter(description = "ticker name of stock", example = "AAPL", required = true)
+            @PathVariable String ticker
+    );
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "404", description = "NOT FOUND",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "섹터 비중 분석",
+            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
+                    required = true,
+                    content = @Content(mediaType = "application/json",
+                            schema = @Schema(implementation = SectorRatioRequest.class),
+                            examples = {
+                                    @ExampleObject(name = "SectorRatioRequestExample", value = "{\"tickerShares\":[{\"ticker\":\"AAPL\",\"share\":3}]}")
+                            })))
+    ResponseEntity<List<SectorRatioResponse>> findSectorRatios(
+            @Valid @RequestBody final SectorRatioRequest request);
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "배당락일이 다가오는 주식 리스트")
+    ResponseEntity<UpcomingDividendResponse> getUpComingDividendStocks(
+            @Parameter(description = "sector value", example = "TECHNOLOGY", required = true)
+            @RequestParam @NotNull final Sector sector,
+            @Parameter(description = "page number(start with 1) for pagination", example = "1", required = true)
+            @RequestParam @NotNull final Integer pageNumber,
+            @Parameter(description = "page size for pagination", example = "20", required = true)
+            @RequestParam @NotNull final Integer pageSize
+    );
+
+    @ApiResponses(value = {
+            @ApiResponse(responseCode = "200", description = "SUCCESS"),
+            @ApiResponse(responseCode = "400", description = "BAD REQUEST",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))}),
+            @ApiResponse(responseCode = "500", description = "SERVER ERROR",
+                    content = {@Content(schema = @Schema(implementation = ErrorResponse.class))})
+    })
+    @Operation(summary = "배당수익률이 큰 주식 리스트")
+    ResponseEntity<StockDividendYieldResponse> getBiggestDividendYieldStocks(
+            @Parameter(description = "sector value", example = "TECHNOLOGY", required = true)
+            @RequestParam @NotNull final Sector sector,
+            @Parameter(description = "page number(start with 1) for pagination", example = "1", required = true)
+            @RequestParam @NotNull final Integer pageNumber,
+            @Parameter(description = "page size for pagination", example = "20", required = true)
+            @RequestParam @NotNull final Integer pageSize
+    );
+}
+
diff --git a/api-server/src/main/resources/application-dev.yml b/api-server/src/main/resources/application-dev.yml
new file mode 100644
index 00000000..dcbdb369
--- /dev/null
+++ b/api-server/src/main/resources/application-dev.yml
@@ -0,0 +1,19 @@
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/nexters
+    username: test
+    password: test
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: validate
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: true
+
+springdoc:
+  swagger-ui:
+    path: /payout-docs.html
+    query-config-enabled: true
+    enabled: true
diff --git a/api-server/src/main/resources/application-prod.yml b/api-server/src/main/resources/application-prod.yml
new file mode 100644
index 00000000..933816a8
--- /dev/null
+++ b/api-server/src/main/resources/application-prod.yml
@@ -0,0 +1,30 @@
+spring:
+  datasource:
+    url: jdbc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE}
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: update
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: false
+
+  flyway:
+    enabled: true
+    baseline-on-migrate: true
+    url: jdbc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE}
+    user: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+    baseline-version: 0
+
+
+springdoc:
+  swagger-ui:
+    path: /payout-docs.html
+    query-config-enabled: true
+    enabled: true
diff --git a/api-server/src/main/resources/application-test.yml b/api-server/src/main/resources/application-test.yml
new file mode 100644
index 00000000..2d69e7a9
--- /dev/null
+++ b/api-server/src/main/resources/application-test.yml
@@ -0,0 +1,15 @@
+spring:
+  datasource:
+    driver-class-name: org.h2.Driver
+    url: jdbc:h2:mem:test;MODE=MySQL
+    username: sa
+    password:
+
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: create-drop
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: true
diff --git a/api-server/src/main/resources/application.properties b/api-server/src/main/resources/application.properties
deleted file mode 100644
index 900d48b2..00000000
--- a/api-server/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.profiles.include=console-logging, file-logging
diff --git a/api-server/src/main/resources/application.yml b/api-server/src/main/resources/application.yml
new file mode 100644
index 00000000..027b4e36
--- /dev/null
+++ b/api-server/src/main/resources/application.yml
@@ -0,0 +1,3 @@
+spring:
+  profiles:
+    active: test
\ No newline at end of file
diff --git a/api-server/src/test/java/nexters/dividend/apiserver/DividendApiServerApplicationTests.java b/api-server/src/test/java/nexters/dividend/apiserver/DividendApiServerApplicationTests.java
deleted file mode 100644
index fc07be99..00000000
--- a/api-server/src/test/java/nexters/dividend/apiserver/DividendApiServerApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package nexters.dividend.apiserver;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class DividendApiServerApplicationTests {
-
-	@Test
-	void contextLoads() {
-	}
-
-}
diff --git a/api-server/src/test/java/nexters/payout/apiserver/dividend/application/DividendQueryServiceTest.java b/api-server/src/test/java/nexters/payout/apiserver/dividend/application/DividendQueryServiceTest.java
new file mode 100644
index 00000000..7ef32034
--- /dev/null
+++ b/api-server/src/test/java/nexters/payout/apiserver/dividend/application/DividendQueryServiceTest.java
@@ -0,0 +1,77 @@
+package nexters.payout.apiserver.dividend.application;
+
+import nexters.payout.apiserver.dividend.application.dto.request.DividendRequest;
+import nexters.payout.apiserver.dividend.application.dto.request.TickerShare;
+import nexters.payout.apiserver.dividend.application.dto.response.MonthlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.YearlyDividendResponse;
+import nexters.payout.apiserver.dividend.common.GivenFixtureTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.junit.jupiter.MockitoExtension;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static nexters.payout.domain.StockFixture.*;
+import static nexters.payout.domain.stock.domain.Sector.*;
+
+@ExtendWith(MockitoExtension.class)
+class DividendQueryServiceTest extends GivenFixtureTest {
+
+    @InjectMocks
+    private DividendQueryService dividendQueryService;
+
+    @Test
+    void 사용자의_월간_배당금_정보를_가져온다() {
+        // given
+        givenStockAndDividendForMonthly(AAPL, TECHNOLOGY, 2.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+        givenStockAndDividendForMonthly(TSLA, UTILITIES, 4.2, 1, 4, 7, 10);
+        givenStockAndDividendForMonthly(SBUX, CONSUMER_CYCLICAL, 5.0, 6, 12);
+        double expected = 86.8;
+
+        // when
+        List<MonthlyDividendResponse> actual = dividendQueryService.getMonthlyDividends(request());
+
+        // then
+        assertAll(
+                () -> assertThat(actual.size()).isEqualTo(12),
+                () -> assertThat(actual
+                        .stream()
+                        .mapToDouble(MonthlyDividendResponse::totalDividend)
+                        .sum()).isEqualTo(expected),
+                () -> assertThat(actual.get(11).dividends().get(0).totalDividend()).isEqualTo(5.0)
+        );
+    }
+
+    @Test
+    void 사용자의_연간_배당금_정보를_가져온다() {
+        // given
+        givenStockAndDividendForYearly(AAPL, TECHNOLOGY, 2.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+        givenStockAndDividendForYearly(TSLA, UTILITIES, 4.2, 1, 4, 7, 10);
+        givenStockAndDividendForYearly(SBUX, CONSUMER_CYCLICAL, 5.0, 6, 12);
+        double totalDividendExpected = 86.8;
+        double aaplDividendExpected = 60.0;
+
+        // when
+        YearlyDividendResponse actual = dividendQueryService.getYearlyDividends(request());
+
+        // then
+        assertAll(
+                () -> assertThat(actual.totalDividend()).isEqualTo(totalDividendExpected),
+                () -> assertThat(actual.dividends()
+                        .stream()
+                        .filter(dividend -> dividend.ticker().equals(AAPL))
+                        .findFirst().get()
+                        .totalDividend())
+                        .isEqualTo(aaplDividendExpected)
+        );
+    }
+
+    private DividendRequest request() {
+        return new DividendRequest(List.of(
+                new TickerShare(AAPL, 2),
+                new TickerShare(TSLA, 1),
+                new TickerShare(SBUX, 1)));
+    }
+}
\ No newline at end of file
diff --git a/api-server/src/test/java/nexters/payout/apiserver/dividend/common/GivenFixtureTest.java b/api-server/src/test/java/nexters/payout/apiserver/dividend/common/GivenFixtureTest.java
new file mode 100644
index 00000000..a13dcebb
--- /dev/null
+++ b/api-server/src/test/java/nexters/payout/apiserver/dividend/common/GivenFixtureTest.java
@@ -0,0 +1,93 @@
+package nexters.payout.apiserver.dividend.common;
+
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.DividendFixture;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+public abstract class GivenFixtureTest {
+
+    private final Integer JANUARY = 1;
+    private final Integer DECEMBER = 12;
+
+    @Mock
+    protected DividendRepository dividendRepository;
+
+    @Mock
+    protected StockRepository stockRepository;
+
+    public void givenStockAndDividendForMonthly(String ticker, Sector sector, double dividend, int... cycle) {
+        Stock stock = StockFixture.createStock(ticker, sector);
+        given(stockRepository.findByTicker(eq(ticker))).willReturn(Optional.of(stock));
+
+        for (int month = JANUARY; month <= DECEMBER; month++) {
+            if (isContain(cycle, month)) {
+                // 배당 주기에 해당하는 경우
+                given(dividendRepository.findAllByTickerAndYearAndMonth(
+                        eq(ticker),
+                        eq(InstantProvider.getLastYear()),
+                        eq(month)))
+                        .willReturn(List.of(DividendFixture.createDividend(
+                                stock.getId(),
+                                dividend,
+                                parseDate(InstantProvider.getLastYear(), month)
+                        )));
+            } else {
+                // 배당 주기에 해당하지 않는 경우
+                given(dividendRepository.findAllByTickerAndYearAndMonth(
+                        eq(ticker),
+                        eq(InstantProvider.getLastYear()),
+                        eq(month)))
+                        .willReturn(new ArrayList<>());
+            }
+        }
+    }
+
+    public void givenStockAndDividendForYearly(String ticker, Sector sector, double dividend, int... cycle) {
+        Stock stock = StockFixture.createStock(ticker, sector);
+        given(stockRepository.findByTicker(eq(ticker))).willReturn(Optional.of(stock));
+
+        List<Dividend> dividends = new ArrayList<>();
+        for (int month : cycle) {
+            dividends.add(DividendFixture.createDividend(
+                    stock.getId(),
+                    dividend,
+                    parseDate(InstantProvider.getLastYear(), month)));
+        }
+
+        given(dividendRepository.findAllByTickerAndYear(
+                eq(ticker),
+                eq(InstantProvider.getLastYear())))
+                .willReturn(dividends);
+    }
+
+    private boolean isContain(int[] cycle, int month) {
+        return Arrays.stream(cycle).anyMatch(m -> m == month);
+    }
+
+    private Instant parseDate(int year, int month) {
+        LocalDate date = LocalDate.of(year, month, 1);
+        ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.of("UTC"));
+        return zonedDateTime.toInstant();
+    }
+}
diff --git a/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java b/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java
new file mode 100644
index 00000000..d86b91f6
--- /dev/null
+++ b/api-server/src/test/java/nexters/payout/apiserver/dividend/presentation/DividendControllerTest.java
@@ -0,0 +1,321 @@
+package nexters.payout.apiserver.dividend.presentation;
+
+import io.restassured.RestAssured;
+import io.restassured.common.mapper.TypeRef;
+import io.restassured.http.ContentType;
+import nexters.payout.apiserver.dividend.application.dto.request.DividendRequest;
+import nexters.payout.apiserver.dividend.application.dto.request.TickerShare;
+import nexters.payout.apiserver.dividend.application.dto.response.MonthlyDividendResponse;
+import nexters.payout.apiserver.dividend.application.dto.response.YearlyDividendResponse;
+import nexters.payout.apiserver.stock.common.IntegrationTest;
+import nexters.payout.core.exception.ErrorResponse;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.DividendFixture;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import static nexters.payout.domain.StockFixture.AAPL;
+import static nexters.payout.domain.StockFixture.TSLA;
+import static org.apache.http.HttpStatus.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+class DividendControllerTest extends IntegrationTest {
+
+    @Test
+    void 월별_배당금_조회시_티커를_찾을수없는경우_404_예외가_발생한다() {
+        // given
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request())
+                .when().post("api/dividends/monthly")
+                .then().log().all()
+                .statusCode(SC_NOT_FOUND)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 연간_배당금_조회시_티커를_찾을수없는경우_404_예외가_발생한다() {
+        // given
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request())
+                .when().post("api/dividends/yearly")
+                .then().log().all()
+                .statusCode(SC_NOT_FOUND)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 월별_배당금_조회시_배당금이_존재하지_않는_경우_정상적으로_조회된다() {
+        // given
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+        stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY));
+        double expected = 0.0;
+
+        // when
+        List<MonthlyDividendResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .request()
+                .body(request())
+                .when().post("api/dividends/monthly")
+                .then().log().all()
+                .statusCode(SC_OK)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        assertAll(
+                () -> assertThat(actual
+                        .stream()
+                        .mapToDouble(MonthlyDividendResponse::totalDividend)
+                        .sum())
+                        .isEqualTo(expected),
+                () -> actual.forEach(res -> assertThat(res.dividends()).isEmpty())
+        );
+    }
+
+    @Test
+    void 연간_배당금_조회시_배당금이_존재하지_않는_경우_정상적으로_조회된다() {
+        // given
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+        stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY));
+        double expected = 0.0;
+
+        // when
+        YearlyDividendResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request())
+                .when().post("api/dividends/yearly")
+                .then().log().all()
+                .statusCode(SC_OK)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.totalDividend()).isEqualTo(expected),
+                () -> assertThat(actual.dividends()).isEmpty()
+        );
+    }
+
+    @Test
+    void 월별_배당금_조회시_배당금이_존재하는_경우_정상적으로_조회된다() {
+        // given
+        stockAndDividendGiven();
+        double expected = 13.0;
+
+        // when
+        List<MonthlyDividendResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request())
+                .when().post("api/dividends/monthly")
+                .then().log().all()
+                .statusCode(SC_OK)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        assertAll(
+                () -> assertThat(actual
+                        .stream()
+                        .mapToDouble(MonthlyDividendResponse::totalDividend)
+                        .sum())
+                        .isEqualTo(expected),
+                () -> assertThat(actual).hasSize(12)
+        );
+    }
+
+    @Test
+    void 연간_배당금_조회시_배당금이_존재하는_경우_정상적으로_조회된다() {
+        // given
+        stockAndDividendGiven();
+        double expected = 13.0;
+
+        // when
+        YearlyDividendResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request())
+                .when().post("api/dividends/yearly")
+                .then().log().all()
+                .statusCode(SC_OK)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.totalDividend()).isEqualTo(expected),
+                () -> assertThat(actual.dividends().size()).isEqualTo(2)
+        );
+    }
+
+    @Test
+    void 월별_배당금_조회시_빈_리스트로_요청한_경우_400_예외가_발생한다() {
+        // given
+        DividendRequest request = new DividendRequest(new ArrayList<>());
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/dividends/monthly")
+                .then().log().all()
+                .statusCode(SC_BAD_REQUEST)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 연간_배당금_조회시_빈_리스트로_요청한_경우_400_예외가_발생한다() {
+        // given
+        DividendRequest request = new DividendRequest(new ArrayList<>());
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/dividends/yearly")
+                .then().log().all()
+                .statusCode(SC_BAD_REQUEST)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 월간_배당금_조회시_티커가_빈문자열이면_예외가_발생한다() {
+        // given
+        DividendRequest request = new DividendRequest(List.of(new TickerShare("", 2)));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/dividends/monthly")
+                .then().log().all()
+                .statusCode(SC_BAD_REQUEST)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 연간_배당금_조회시_티커가_빈문자열이면_예외가_발생한다() {
+        // given
+        DividendRequest request = new DividendRequest(List.of(new TickerShare("", 2)));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/dividends/yearly")
+                .then().log().all()
+                .statusCode(SC_BAD_REQUEST)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 월간_배당금_조회시__종목_소유_개수가_0개인_경우_400_예외가_발생한다() {
+        // given
+        DividendRequest request = new DividendRequest(List.of(new TickerShare(AAPL, 0)));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/dividends/monthly")
+                .then().log().all()
+                .statusCode(SC_BAD_REQUEST)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 연간_배당금_조회시__종목_소유_개수가_0개인_경우_400_예외가_발생한다() {
+        // given
+        DividendRequest request = new DividendRequest(List.of(new TickerShare(AAPL, 0)));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/dividends/yearly")
+                .then().log().all()
+                .statusCode(SC_BAD_REQUEST)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    private DividendRequest request() {
+        return new DividendRequest(List.of(
+                new TickerShare(AAPL, 2),
+                new TickerShare(TSLA, 1)
+        ));
+    }
+
+    private void stockAndDividendGiven() {
+        Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY));
+        Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                2.5,
+                parseDate(InstantProvider.getLastYear(), 1)));
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                2.5,
+                parseDate(InstantProvider.getLastYear(), 6)));
+        dividendRepository.save(DividendFixture.createDividend(
+                tsla.getId(),
+                3.0,
+                parseDate(InstantProvider.getLastYear(), 6)));
+    }
+
+    private Instant parseDate(int year, int month) {
+        LocalDate date = LocalDate.of(year, month, 1);
+        ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.of("UTC"));
+        return zonedDateTime.toInstant();
+    }
+}
\ No newline at end of file
diff --git a/api-server/src/test/java/nexters/payout/apiserver/stock/application/StockQueryServiceTest.java b/api-server/src/test/java/nexters/payout/apiserver/stock/application/StockQueryServiceTest.java
new file mode 100644
index 00000000..fff4ac0b
--- /dev/null
+++ b/api-server/src/test/java/nexters/payout/apiserver/stock/application/StockQueryServiceTest.java
@@ -0,0 +1,202 @@
+package nexters.payout.apiserver.stock.application;
+
+import nexters.payout.apiserver.stock.application.dto.request.SectorRatioRequest;
+import nexters.payout.apiserver.stock.application.dto.request.TickerShare;
+import nexters.payout.apiserver.stock.application.dto.response.*;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.apiserver.stock.application.dto.response.SingleUpcomingDividendResponse;
+import nexters.payout.apiserver.stock.application.dto.response.SectorRatioResponse;
+import nexters.payout.apiserver.stock.application.dto.response.StockDetailResponse;
+import nexters.payout.apiserver.stock.application.dto.response.StockResponse;
+import nexters.payout.domain.DividendFixture;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import nexters.payout.domain.stock.infra.dto.StockDividendDto;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import nexters.payout.domain.stock.domain.service.StockDividendAnalysisService;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService;
+import nexters.payout.domain.stock.infra.dto.StockDividendYieldDto;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.List;
+import java.util.Optional;
+
+import static java.time.ZoneOffset.UTC;
+import static nexters.payout.domain.StockFixture.*;
+import static nexters.payout.domain.stock.domain.Sector.TECHNOLOGY;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+class StockQueryServiceTest {
+
+    @InjectMocks
+    private StockQueryService stockQueryService;
+
+    @Mock
+    private StockRepository stockRepository;
+    @Mock
+    private DividendRepository dividendRepository;
+    @Spy
+    private SectorAnalysisService sectorAnalysisService;
+    @Spy
+    private StockDividendAnalysisService stockDividendAnalysisService;
+
+    @Test
+    void 검색된_종목_정보를_정상적으로_반환한다() {
+        // given
+        given(stockRepository.findStocksByTickerOrNameWithPriority(any(), any(), any())).willReturn(List.of(StockFixture.createStock(AAPL, Sector.TECHNOLOGY)));
+
+        // when
+        List<StockResponse> actual = stockQueryService.searchStock("A", 1, 2);
+
+        // then
+        assertAll(
+                () -> assertThat(actual.get(0).ticker()).isEqualTo(AAPL),
+                () -> assertThat(actual.get(0).sectorName()).isEqualTo(Sector.TECHNOLOGY.getName()),
+                () -> assertThat(actual.get(0).logoUrl()).isEqualTo("")
+        );
+    }
+
+    @Test
+    void 종목_상세_정보를_정상적으로_반환한다() {
+        // given
+        int lastYear = LocalDate.now(UTC).getYear() - 1;
+        int expectedMonth = 3;
+        Instant exDividendDate = LocalDate.of(lastYear, expectedMonth, 1).atStartOfDay().toInstant(UTC);
+        Double expectedPrice = 2.0;
+        Double expectedDividend = 0.5;
+        Stock aapl = StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 2.0);
+        Dividend dividend = DividendFixture.createDividend(aapl.getId(), 0.5, exDividendDate);
+
+        given(stockRepository.findByTicker(any())).willReturn(Optional.of(aapl));
+        given(dividendRepository.findAllByStockId(any())).willReturn(List.of(dividend));
+
+        // when
+        StockDetailResponse actual = stockQueryService.getStockByTicker(aapl.getTicker());
+
+        // then
+        assertAll(
+                () -> assertThat(actual.ticker()).isEqualTo(aapl.getTicker()),
+                () -> assertThat(actual.industry()).isEqualTo(aapl.getIndustry()),
+                () -> assertThat(actual.dividendYield()).isEqualTo(expectedDividend / expectedPrice),
+                () -> assertThat(actual.dividendMonths()).isEqualTo(List.of(Month.of(expectedMonth)))
+        );
+    }
+
+    @Test
+    void 종목_상세_정보의_배당날짜를_올해기준으로_반환한다() {
+        // given
+        int expectedMonth = 3;
+        int expectedDayOfMonth = 1;
+        int lastYear = LocalDate.now().getYear() - 1;
+        Instant exDividendDate = LocalDate.of(lastYear, 3, 1).atStartOfDay().toInstant(UTC);
+        Stock appl = StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 2.0);
+        Dividend dividend = DividendFixture.createDividendWithPaymentDate(appl.getId(), 0.5, exDividendDate);
+
+        given(stockRepository.findByTicker(any())).willReturn(Optional.of(appl));
+        given(dividendRepository.findAllByStockId(any())).willReturn(List.of(dividend));
+
+        // when
+        StockDetailResponse actual = stockQueryService.getStockByTicker(appl.getTicker());
+
+        // then
+        assertThat(actual.earliestPaymentDate()).isEqualTo(LocalDate.of(lastYear + 1, expectedMonth, expectedDayOfMonth));
+    }
+
+    @Test
+    void 섹터_정보를_정상적으로_반환한다() {
+        // given
+        SectorRatioRequest request = new SectorRatioRequest(List.of(new TickerShare(AAPL, 2), new TickerShare(TSLA, 3)));
+        Stock appl = StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 4.0);
+        Stock tsla = StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL, 2.2);
+        List<Stock> stocks = List.of(appl, tsla);
+
+        given(stockRepository.findAllByTickerIn(any())).willReturn(stocks);
+
+        List<SectorRatioResponse> expected = List.of(
+                new SectorRatioResponse(
+                        Sector.TECHNOLOGY.getName(),
+                        Sector.TECHNOLOGY.name(),
+                        0.547945205479452,
+                        List.of(new StockShareResponse(
+                                StockResponse.from(appl),
+                                2
+                        ))
+                ),
+                new SectorRatioResponse(
+                        Sector.CONSUMER_CYCLICAL.getName(),
+                        Sector.CONSUMER_CYCLICAL.name(),
+                        0.4520547945205479,
+                        List.of(new StockShareResponse(
+                                StockResponse.from(tsla),
+                                3
+                        ))
+                )
+        );
+
+        // when
+        List<SectorRatioResponse> actual = stockQueryService.analyzeSectorRatio(request);
+
+        // then
+        assertThat(actual).containsExactlyInAnyOrderElementsOf(expected);
+    }
+
+    @Test
+    void 배당락일이_다가오는_주식_리스트를_가져온다() {
+        // given
+        Stock stock = StockFixture.createStock(AAPL, TECHNOLOGY);
+        Dividend expected = DividendFixture.createDividendWithExDividendDate(stock.getId(), LocalDateTime.now().plusDays(1).toInstant(UTC));
+        given(stockRepository.findUpcomingDividendStock(TECHNOLOGY, 1, 10))
+                .willReturn(List.of(new StockDividendDto(stock, expected)));
+
+        // when
+        List<SingleUpcomingDividendResponse> actual = stockQueryService.getUpcomingDividendStocks(TECHNOLOGY, 1, 10).dividends();
+
+        // then
+        assertAll(
+                () -> assertThat(actual.size()).isEqualTo(1),
+                () -> assertThat(actual.get(0).exDividendDate()).isEqualTo(expected.getExDividendDate()),
+                () -> assertThat(actual.get(0).ticker()).isEqualTo(stock.getTicker())
+        );
+    }
+
+    @Test
+    void 배당_수익률이_큰_순서대로_주식_리스트를_가져온다() {
+        // given
+        Stock expected = StockFixture.createStock(AAPL, TECHNOLOGY, 2.0);
+        Stock tsla = StockFixture.createStock(TSLA, TECHNOLOGY, 3.0);
+        given(stockRepository.findBiggestDividendYieldStock(InstantProvider.getLastYear(), TECHNOLOGY, 1, 10))
+                .willReturn(List.of(
+                        new StockDividendYieldDto(expected, 5.0),
+                        new StockDividendYieldDto(tsla, 4.0))
+                );
+        Double expectedAaplDividendYield = 5.0;
+
+        // when
+        List<SingleStockDividendYieldResponse> actual = stockQueryService.getBiggestDividendStocks(TECHNOLOGY, 1, 10).dividends();
+
+
+        // then
+        assertAll(
+                () -> assertThat(actual.size()).isEqualTo(2),
+                () -> assertThat(actual.get(0).stockId()).isEqualTo(expected.getId()),
+                () -> assertThat(actual.get(0).dividendYield()).isEqualTo(expectedAaplDividendYield)
+        );
+    }
+}
\ No newline at end of file
diff --git a/api-server/src/test/java/nexters/payout/apiserver/stock/common/IntegrationTest.java b/api-server/src/test/java/nexters/payout/apiserver/stock/common/IntegrationTest.java
new file mode 100644
index 00000000..ae5aa6bf
--- /dev/null
+++ b/api-server/src/test/java/nexters/payout/apiserver/stock/common/IntegrationTest.java
@@ -0,0 +1,36 @@
+package nexters.payout.apiserver.stock.common;
+
+import io.restassured.RestAssured;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.web.server.LocalServerPort;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("test")
+public abstract class IntegrationTest {
+
+    @LocalServerPort
+    private int port;
+
+    @Autowired
+    public StockRepository stockRepository;
+
+    @Autowired
+    public DividendRepository dividendRepository;
+
+    @BeforeEach
+    void setUp() {
+        RestAssured.port = port;
+    }
+
+    @AfterEach
+    void afterEach() {
+        dividendRepository.deleteAll();
+        stockRepository.deleteAll();
+    }
+}
diff --git a/api-server/src/test/java/nexters/payout/apiserver/stock/presentation/integration/StockControllerTest.java b/api-server/src/test/java/nexters/payout/apiserver/stock/presentation/integration/StockControllerTest.java
new file mode 100644
index 00000000..68846ad3
--- /dev/null
+++ b/api-server/src/test/java/nexters/payout/apiserver/stock/presentation/integration/StockControllerTest.java
@@ -0,0 +1,512 @@
+package nexters.payout.apiserver.stock.presentation.integration;
+
+import io.restassured.RestAssured;
+import io.restassured.common.mapper.TypeRef;
+import io.restassured.http.ContentType;
+import nexters.payout.apiserver.stock.application.dto.request.SectorRatioRequest;
+import nexters.payout.apiserver.stock.application.dto.request.TickerShare;
+import nexters.payout.apiserver.stock.application.dto.response.*;
+import nexters.payout.apiserver.stock.common.IntegrationTest;
+import nexters.payout.core.exception.ErrorResponse;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.DividendFixture;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Month;
+import java.util.List;
+
+import static java.time.ZoneOffset.UTC;
+import static nexters.payout.core.time.InstantProvider.*;
+import static nexters.payout.domain.StockFixture.*;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class StockControllerTest extends IntegrationTest {
+    @Test
+    void 검색키워드가_빈값인_경우_400_예외가_발생한다() {
+        // given
+        Stock apdd = StockFixture.createStock("APDD", "DDDD");
+
+        stockRepository.save(apdd);
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/search?keyword=")
+                .then().log().all()
+                .statusCode(400)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 티커는_앞자리부터_검색_회사명은_중간에서도_검색_가능하다() {
+        // given
+        Stock apdd = StockFixture.createStock("APDD", "DDDD");
+        Stock abcd = StockFixture.createStock("ABCD", "APPLE");
+
+        stockRepository.save(apdd);
+        stockRepository.save(abcd);
+
+        // when
+        List<StockResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/search?keyword=AP&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual).hasSize(2),
+                () -> assertThat(actual).containsExactlyInAnyOrderElementsOf(
+                        List.of(
+                                new StockResponse(apdd.getId(), apdd.getTicker(), apdd.getName(), apdd.getSector().getName(), apdd.getSector().name(), apdd.getExchange(), apdd.getIndustry(), apdd.getPrice(), apdd.getVolume(), apdd.getLogoUrl()),
+                                new StockResponse(abcd.getId(), abcd.getTicker(), abcd.getName(), abcd.getSector().getName(), abcd.getSector().name(), abcd.getExchange(), abcd.getIndustry(), abcd.getPrice(), abcd.getVolume(), abcd.getLogoUrl())
+                        )
+                )
+        );
+    }
+
+    @Test
+    void 티커_기반_검색_1순위_회사명_기반_검색이_2순위이다() {
+        // given
+        Stock apdd = StockFixture.createStock("APDD", "DDDD");
+        Stock abcd = StockFixture.createStock("ABCD", "APPLE");
+
+        stockRepository.save(apdd);
+        stockRepository.save(abcd);
+
+        // when
+        List<StockResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/search?keyword=AP&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual).hasSize(2),
+                () -> assertThat(actual).isEqualTo(
+                        List.of(
+                                new StockResponse(apdd.getId(), apdd.getTicker(), apdd.getName(), apdd.getSector().getName(), apdd.getSector().name(), apdd.getExchange(), apdd.getIndustry(), apdd.getPrice(), apdd.getVolume(), apdd.getLogoUrl()),
+                                new StockResponse(abcd.getId(), abcd.getTicker(), abcd.getName(), abcd.getSector().getName(), abcd.getSector().name(), abcd.getExchange(), abcd.getIndustry(), abcd.getPrice(), abcd.getVolume(), abcd.getLogoUrl())
+                        )
+                )
+        );
+    }
+
+    @Test
+    void 검색_결과는_알파벳_순으로_정렬한다() {
+        // given
+        Stock dddd = StockFixture.createStock("DDDD", "DDDDA");
+        Stock aaaa = StockFixture.createStock("AAAA", "AAADA");
+
+        stockRepository.save(dddd);
+        stockRepository.save(aaaa);
+
+        // when
+        List<StockResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/search?keyword=DA&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual).hasSize(2),
+                () -> assertThat(actual).containsExactlyInAnyOrderElementsOf(
+                        List.of(
+                                new StockResponse(aaaa.getId(), aaaa.getTicker(), aaaa.getName(), aaaa.getSector().getName(), aaaa.getSector().name(), aaaa.getExchange(), aaaa.getIndustry(), aaaa.getPrice(), aaaa.getVolume(), aaaa.getLogoUrl()),
+                                new StockResponse(dddd.getId(), dddd.getTicker(), dddd.getName(), dddd.getSector().getName(), dddd.getSector().name(), dddd.getExchange(), dddd.getIndustry(), dddd.getPrice(), dddd.getVolume(), dddd.getLogoUrl())
+                        )
+                )
+        );
+    }
+
+    @Test
+    void 종목_조회시_티커를_찾을수없는경우_404_예외가_발생한다() {
+        // given
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/aaaaa")
+                .then().log().all()
+                .statusCode(404)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 종목_조회시_종목의_정보가_정상적으로_조회된다() {
+        // given
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL));
+
+        // when, then
+        StockDetailResponse stockDetailResponse = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/TSLA")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        assertAll(
+                () -> assertThat(stockDetailResponse.ticker()).isEqualTo(TSLA),
+                () -> assertThat(stockDetailResponse.sectorName()).isEqualTo(Sector.CONSUMER_CYCLICAL.getName())
+        );
+    }
+
+    @Test
+    void 종목_조회시_종목의_현재가가_존재하지않으면_배당수익률은_0으로_조회된다() {
+        // given
+        Double price = null;
+        Double dividend = 12.0;
+        Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.CONSUMER_CYCLICAL, price));
+        Instant paymentDate = LocalDate.of(2023, 4, 5).atStartOfDay().toInstant(UTC);
+        dividendRepository.save(DividendFixture.createDividendWithPaymentDate(tsla.getId(), dividend, paymentDate));
+
+        // when, then
+        StockDetailResponse stockDetailResponse = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/TSLA")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        assertAll(
+                () -> assertThat(stockDetailResponse.dividendPerShare()).isEqualTo(dividend),
+                () -> assertThat(stockDetailResponse.dividendYield()).isEqualTo(0),
+                () -> assertThat(stockDetailResponse.earliestPaymentDate()).isEqualTo(LocalDate.of(LocalDate.now().getYear(), 4, 5)),
+                () -> assertThat(stockDetailResponse.dividendMonths()).isEqualTo(List.of(Month.APRIL))
+        );
+    }
+
+
+    @Test
+    void 섹터_분석시_빈_리스트로_요청한_경우_400_예외가_발생한다() {
+        // given
+        SectorRatioRequest request = new SectorRatioRequest(List.of());
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/stocks/sector-ratio")
+                .then().log().all()
+                .statusCode(400)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 섹터_분석시_티커가_빈문자열이면_예외가_발생한다() {
+        // given
+        SectorRatioRequest request = new SectorRatioRequest(List.of(new TickerShare("", 1)));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/stocks/sector-ratio")
+                .then().log().all()
+                .statusCode(400)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+    @Test
+    void 섹터_분석시_종목_소유_개수가_0개인_경우_400_예외가_발생한다() {
+        // given
+        SectorRatioRequest request = new SectorRatioRequest(List.of(new TickerShare(AAPL, 0)));
+
+        // when, then
+        RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/stocks/sector-ratio")
+                .then().log().all()
+                .statusCode(400)
+                .extract()
+                .as(ErrorResponse.class);
+    }
+
+
+    @Test
+    void 섹터_분석시_티커가_1개_이상일_경우_정상적으로_동작한다() {
+        // given
+        SectorRatioRequest request = new SectorRatioRequest(List.of(new TickerShare(AAPL, 2)));
+        Stock stock = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 5.0));
+
+        // when
+        List<SectorRatioResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/stocks/sector-ratio")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual).hasSize(1),
+                () -> assertThat(actual.get(0).sectorName()).isEqualTo("Technology"),
+                () -> assertThat(actual.get(0).sectorRatio()).isEqualTo(1.0),
+                () -> assertThat(actual.get(0).stockShares().get(0).stockResponse().ticker()).isEqualTo(AAPL)
+        );
+    }
+
+    @Test
+    void 섹터_분석시_선택한_종목의_배당금이_존재하지_않아도_정상적으로_동작한다() {
+        // given
+        SectorRatioRequest request = new SectorRatioRequest(List.of(new TickerShare(AAPL, 2)));
+        Stock stock = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 5.0));
+
+        // when
+        List<SectorRatioResponse> actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .body(request)
+                .when().post("api/stocks/sector-ratio")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual).hasSize(1),
+                () -> assertThat(actual.get(0).sectorName()).isEqualTo("Technology"),
+                () -> assertThat(actual.get(0).sectorRatio()).isEqualTo(1.0),
+                () -> assertThat(actual.get(0).stockShares().get(0).stockResponse().ticker()).isEqualTo(AAPL)
+        );
+    }
+
+    @Test
+    void 배당락일이_다가오는_주식_리스트를_가져온다() {
+        // given
+        Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 5.0));
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                25.0,
+                LocalDateTime.now().plusDays(1).toInstant(UTC)
+        ));
+        LocalDateTime expected = LocalDateTime.now().plusDays(1);
+
+        // when
+        UpcomingDividendResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/ex-dividend-dates/upcoming?sector=TECHNOLOGY&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.dividends().size()).isEqualTo(1),
+                () -> assertThat(actual.dividends().get(0).stockId()).isEqualTo(aapl.getId()),
+                () -> assertThat(getYear(actual.dividends().get(0).exDividendDate())).isEqualTo(expected.getYear()),
+                () -> assertThat(getMonth(actual.dividends().get(0).exDividendDate())).isEqualTo(expected.getMonthValue()),
+                () -> assertThat(getDayOfMonth(actual.dividends().get(0).exDividendDate())).isEqualTo(expected.getDayOfMonth())
+        );
+    }
+
+    @Test
+    void 배당락일이_다가오는_주식_리스트는_배당락일이_가까운_순서대로_정렬된다() {
+        // given
+        Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 5.0));
+        Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.TECHNOLOGY, 5.0));
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                25.0,
+                LocalDateTime.now().plusDays(2).toInstant(UTC)
+        ));
+        dividendRepository.save(DividendFixture.createDividend(
+                tsla.getId(),
+                30.0,
+                LocalDateTime.now().plusDays(1).toInstant(UTC)
+        ));
+        LocalDateTime expected = LocalDateTime.now().plusDays(1);
+
+        // when
+        UpcomingDividendResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/ex-dividend-dates/upcoming?sector=TECHNOLOGY&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.dividends().size()).isEqualTo(2),
+                () -> assertThat(actual.dividends().get(0).stockId()).isEqualTo(tsla.getId()),
+                () -> assertThat(getYear(actual.dividends().get(0).exDividendDate())).isEqualTo(expected.getYear()),
+                () -> assertThat(getMonth(actual.dividends().get(0).exDividendDate())).isEqualTo(expected.getMonthValue()),
+                () -> assertThat(getDayOfMonth(actual.dividends().get(0).exDividendDate())).isEqualTo(expected.getDayOfMonth())
+        );
+    }
+
+    @Test
+    void 배당_수익률이_큰_순서대로_주식_리스트를_가져온다() {
+        // given
+        Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 10.0));
+        Stock tsla = stockRepository.save(StockFixture.createStock(TSLA, Sector.TECHNOLOGY, 20.0));
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                8.0,
+                LocalDate.of(InstantProvider.getLastYear(), 3, 1).atStartOfDay().toInstant(UTC)
+        ));
+        dividendRepository.save(DividendFixture.createDividend(
+                tsla.getId(),
+                5.0,
+                LocalDate.of(InstantProvider.getLastYear(), 3, 1).atStartOfDay().toInstant(UTC)
+        ));
+        dividendRepository.save(DividendFixture.createDividend(
+                tsla.getId(),
+                5.0,
+                LocalDate.of(InstantProvider.getLastYear(), 6, 1).atStartOfDay().toInstant(UTC)
+        ));
+        dividendRepository.save(DividendFixture.createDividend(
+                tsla.getId(),
+                5.0,
+                LocalDate.of(InstantProvider.getLastYear() - 1, 6, 1).atStartOfDay().toInstant(UTC)
+        ));
+
+        Double expectedAaplDividendYield = 0.8;
+        Double expectedTslaDividendYield = 0.5;
+
+        // when
+        StockDividendYieldResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/dividend-yields/highest?sector=TECHNOLOGY&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.dividends().size()).isEqualTo(2),
+                () -> assertThat(actual.dividends().get(0).dividendYield()).isEqualTo(expectedAaplDividendYield),
+                () -> assertThat(actual.dividends().get(0).ticker()).isEqualTo(aapl.getTicker()),
+                () -> assertThat(actual.dividends().get(1).dividendYield()).isEqualTo(expectedTslaDividendYield)
+        );
+    }
+
+    @Test
+    void 연간_배당금이_없는_주식은_배당_수익률_계산시_포함되지_않는다() {
+        // given
+        Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 5.0));
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.TECHNOLOGY, 0.0));
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                2.5,
+                LocalDate.of(InstantProvider.getLastYear(), 3, 1).atStartOfDay().toInstant(UTC)
+        ));
+        Double expected = 0.5;
+
+        // when
+        StockDividendYieldResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/dividend-yields/highest?sector=TECHNOLOGY&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.dividends().size()).isEqualTo(1),
+                () -> assertThat(actual.dividends().get(0).dividendYield()).isEqualTo(expected)
+        );
+    }
+
+    @Test
+    void 배당_수익률이_0_9를_넘어가는_주식은_배당_수익률_계산시_포함되지_않는다() {
+        // given
+        Stock aapl = stockRepository.save(StockFixture.createStock(AAPL, Sector.TECHNOLOGY, 5.0));
+        stockRepository.save(StockFixture.createStock(TSLA, Sector.TECHNOLOGY, 0.0));
+        dividendRepository.save(DividendFixture.createDividend(
+                aapl.getId(),
+                10.0,
+                LocalDate.of(InstantProvider.getLastYear(), 3, 1).atStartOfDay().toInstant(UTC)
+        ));
+
+        // when
+        StockDividendYieldResponse actual = RestAssured
+                .given()
+                .log().all()
+                .contentType(ContentType.JSON)
+                .when().get("api/stocks/dividend-yields/highest?sector=TECHNOLOGY&pageNumber=1&pageSize=20")
+                .then().log().all()
+                .statusCode(200)
+                .extract()
+                .as(new TypeRef<>() {
+                });
+
+        // then
+        assertAll(
+                () -> assertThat(actual.dividends().size()).isEqualTo(0)
+        );
+    }
+}
\ No newline at end of file
diff --git a/batch/Dockerfile b/batch/Dockerfile
new file mode 100644
index 00000000..3eaf668e
--- /dev/null
+++ b/batch/Dockerfile
@@ -0,0 +1,5 @@
+FROM eclipse-temurin:17
+
+ARG JAR_FILE=build/libs/batch.jar
+COPY ${JAR_FILE} batch.jar
+ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod,file-logging","-Duser.timezone=UTC","/batch.jar"]
\ No newline at end of file
diff --git a/batch/build.gradle b/batch/build.gradle
index 7587ea9a..99ac609f 100644
--- a/batch/build.gradle
+++ b/batch/build.gradle
@@ -4,11 +4,40 @@ plugins {
     id 'io.spring.dependency-management' version '1.1.4'
 }
 
+group = 'nexters'
+version = '0.0.1-SNAPSHOT'
+
 dependencies {
-    implementation(project(":core"))
 
+    // include other modules
+    implementation project(":domain")
+    implementation project(":core")
+
+    // Lombok
+    compileOnly 'org.projectlombok:lombok'
+    annotationProcessor 'org.projectlombok:lombok'
+
+    // Spring data jpa
+    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+
+    // Spring boot
     implementation 'org.springframework.boot:spring-boot-starter'
+    
+    // Webflux
+    implementation 'org.springframework.boot:spring-boot-starter-webflux'
+
+    // Spring boot starter
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    testImplementation(testFixtures(project(":domain")))
+
+    // MySQL
+    runtimeOnly 'com.mysql:mysql-connector-j'
+
+    // H2 Database
+    runtimeOnly 'com.h2database:h2'
+
+    // await
+    testImplementation 'org.awaitility:awaitility:4.2.0'
 }
 
 tasks.named('test') {
diff --git a/batch/src/main/java/nexters/dividend/batch/DividendBatchApplication.java b/batch/src/main/java/nexters/dividend/batch/DividendBatchApplication.java
deleted file mode 100644
index 2159dda8..00000000
--- a/batch/src/main/java/nexters/dividend/batch/DividendBatchApplication.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package nexters.dividend.batch;
-
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-public class DividendBatchApplication {
-
-    public static void main(String[] args) {
-        SpringApplication.run(DividendBatchApplication.class, args);
-    }
-
-}
diff --git a/batch/src/main/java/nexters/payout/batch/PayoutBatchApplication.java b/batch/src/main/java/nexters/payout/batch/PayoutBatchApplication.java
new file mode 100644
index 00000000..0e8f8aa8
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/PayoutBatchApplication.java
@@ -0,0 +1,21 @@
+package nexters.payout.batch;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@ConfigurationPropertiesScan
+@SpringBootApplication(scanBasePackages = {
+        "nexters.payout.core",
+        "nexters.payout.domain",
+        "nexters.payout.batch"
+})
+@EnableScheduling
+public class PayoutBatchApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(PayoutBatchApplication.class, args);
+    }
+
+}
diff --git a/batch/src/main/java/nexters/payout/batch/application/DividendBatchService.java b/batch/src/main/java/nexters/payout/batch/application/DividendBatchService.java
new file mode 100644
index 00000000..a277fe10
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/application/DividendBatchService.java
@@ -0,0 +1,67 @@
+package nexters.payout.batch.application;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import nexters.payout.batch.application.FinancialClient.DividendData;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.application.DividendCommandService;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class DividendBatchService {
+
+    private final FinancialClient financialClient;
+    private final DividendCommandService dividendCommandService;
+    private final StockRepository stockRepository;
+
+    /**
+     * UTC 시간대 기준으로 매주 월요일 새벽 4시에 작년 한해 동안의 배당금 정보를 갱신합니다.
+     */
+    @Scheduled(cron = "${schedules.cron.dividend.past}", zone = "UTC")
+    public void updatePastDividendInfo() {
+        log.info("update past dividend start..");
+        handleDividendData(financialClient.getPastDividendList());
+        log.info("update past dividend end..");
+    }
+
+    /**
+     * 어제 삽입된 미래 배당금 정보를 삭제하고, UTC 시간대 기준으로 매일 새벽 4시에 현재 날짜로부터 3개월 간의 다가오는 배당금 정보를 갱신합니다.
+     */
+    @Scheduled(cron = "${schedules.cron.dividend.future}", zone = "UTC")
+    public void updateUpcomingDividendInfo() {
+        log.info("update upcoming dividend start..");
+        dividendCommandService.deleteInvalidDividend();
+        handleDividendData(financialClient.getUpcomingDividendList());
+        log.info("update upcoming dividend end..");
+    }
+
+    private void saveOrUpdateDividendData(final Stock stock, final DividendData dividendData) {
+        try {
+            dividendCommandService.saveOrUpdate(
+                    stock.getId(),
+                    Dividend.create(
+                            stock.getId(), dividendData.dividend(), dividendData.exDividendDate(),
+                            dividendData.paymentDate(), dividendData.declarationDate()
+                    )
+            );
+        } catch (Exception e) {
+            log.error("fail to save(update) dividend: " + dividendData);
+            log.error(e.getMessage());
+        }
+    }
+
+    private void handleDividendData(List<DividendData> dividendResponses) {
+        for (DividendData dividendData : dividendResponses) {
+            stockRepository.findByTicker(dividendData.symbol())
+                    .ifPresent(stock -> saveOrUpdateDividendData(stock, dividendData));
+        }
+    }
+}
diff --git a/batch/src/main/java/nexters/payout/batch/application/FinancialClient.java b/batch/src/main/java/nexters/payout/batch/application/FinancialClient.java
new file mode 100644
index 00000000..5f01f627
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/application/FinancialClient.java
@@ -0,0 +1,47 @@
+package nexters.payout.batch.application;
+
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.time.Instant;
+import java.util.List;
+
+public interface FinancialClient {
+
+    List<StockData> getLatestStockList();
+
+    List<DividendData> getPastDividendList();
+
+    List<DividendData> getUpcomingDividendList();
+
+    record StockData(
+            String ticker,
+            String name,
+            String exchange,
+            Sector sector,
+            String industry,
+            Double price,
+            Integer volume,
+            Integer avgVolume
+    ) {
+        Stock toDomain() {
+            return new Stock(ticker, name, sector, exchange, industry, price, volume, null);
+        }
+
+        Stock toDomain(String logoUrl) {
+            return new Stock(ticker, name, sector, exchange, industry, price, volume, logoUrl);
+        }
+    }
+
+    record DividendData(
+            Instant exDividendDate,
+            String label,
+            Double adjDividend,
+            String symbol,
+            Double dividend,
+            Instant recordDate,
+            Instant paymentDate,
+            Instant declarationDate
+    ) {
+    }
+}
\ No newline at end of file
diff --git a/batch/src/main/java/nexters/payout/batch/application/StockBatchService.java b/batch/src/main/java/nexters/payout/batch/application/StockBatchService.java
new file mode 100644
index 00000000..8aed70e9
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/application/StockBatchService.java
@@ -0,0 +1,45 @@
+package nexters.payout.batch.application;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import nexters.payout.batch.application.FinancialClient.StockData;
+import nexters.payout.domain.stock.application.StockCommandService;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class StockBatchService {
+
+    private final FinancialClient financialClient;
+    private final StockCommandService stockCommandService;
+    private final StockLogo stockLogo;
+    private final StockRepository stockRepository;
+
+    /**
+     * UTC 시간대 기준 매일 자정에 모든 종목의 현재가와 거래량을 업데이트합니다.
+     */
+    @Scheduled(cron = "${schedules.cron.stock}", zone = "UTC")
+    void run() {
+        log.info("update stock start..");
+        List<StockData> stockList = financialClient.getLatestStockList();
+
+        for (StockData stockData : stockList) {
+            try {
+                stockRepository.findByTicker(stockData.ticker()).ifPresentOrElse(
+                        existing -> stockCommandService.update(stockData.ticker(), stockData.toDomain()),
+                        () -> stockCommandService.create(stockData.toDomain(stockLogo.getLogoUrl(stockData.ticker())))
+                );
+            } catch (Exception e) {
+                log.error("fail to save(update) stock: " + stockData);
+                log.error(e.getMessage());
+            }
+        }
+
+        log.info("update stock end..");
+    }
+}
diff --git a/batch/src/main/java/nexters/payout/batch/application/StockLogo.java b/batch/src/main/java/nexters/payout/batch/application/StockLogo.java
new file mode 100644
index 00000000..46eaa42a
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/application/StockLogo.java
@@ -0,0 +1,5 @@
+package nexters.payout.batch.application;
+
+public interface StockLogo {
+    String getLogoUrl(String ticker);
+}
diff --git a/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpDto.java b/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpDto.java
new file mode 100644
index 00000000..3ed1fea1
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpDto.java
@@ -0,0 +1,46 @@
+package nexters.payout.batch.infra.fmp;
+
+
+import lombok.Getter;
+import lombok.Setter;
+import nexters.payout.batch.application.FinancialClient.DividendData;
+import nexters.payout.core.time.DateFormat;
+
+@Getter
+class FmpStockData {
+    String symbol;
+    String companyName;
+    String exchangeShortName;
+    Double price;
+    Integer volume;
+    @Setter
+    String sector;
+    String industry;
+}
+
+record FmpVolumeData(
+        String symbol,
+        Integer volume,
+        Integer avgVolume
+) {
+}
+
+record FmpDividendData(
+        String date,
+        String label,
+        Double adjDividend,
+        String symbol,
+        Double dividend,
+        String recordDate,
+        String paymentDate,
+        String declarationDate
+) {
+    DividendData toDividendData() {
+        return new DividendData(
+                DateFormat.parseInstant(date),
+                label, adjDividend, symbol, dividend,
+                DateFormat.parseInstant(recordDate),
+                DateFormat.parseInstant(paymentDate),
+                DateFormat.parseInstant(declarationDate));
+    }
+}
\ No newline at end of file
diff --git a/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpFinancialClient.java b/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpFinancialClient.java
new file mode 100644
index 00000000..70d11f2f
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpFinancialClient.java
@@ -0,0 +1,206 @@
+package nexters.payout.batch.infra.fmp;
+
+import lombok.extern.slf4j.Slf4j;
+import nexters.payout.batch.application.FinancialClient;
+import nexters.payout.core.time.DateFormat;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.stock.domain.Exchange;
+import nexters.payout.domain.stock.domain.Sector;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.time.ZoneOffset.UTC;
+import static nexters.payout.domain.stock.domain.Sector.ETC;
+import static nexters.payout.domain.stock.domain.Sector.ETF;
+
+@Slf4j
+@Service
+public class FmpFinancialClient implements FinancialClient {
+
+    private final WebClient fmpWebClient;
+    private final FmpProperties fmpProperties;
+    private final static int MAX_LIMIT = 1000000;
+
+    FmpFinancialClient(final FmpProperties fmpProperties) {
+        this.fmpProperties = fmpProperties;
+        this.fmpWebClient = WebClient.builder()
+                .baseUrl(fmpProperties.getBaseUrl())
+                .build();
+    }
+
+    @Override
+    public List<StockData> getLatestStockList() {
+        Map<String, FmpStockData> stockDataMap = Stream.concat(
+                        Sector.getNames()
+                                .stream()
+                                .filter(sector -> !(sector.equals(ETC.getName()) || sector.equals(ETF.getName())))
+                                .flatMap(this::fetchStockList)
+                        , fetchEtfStockList())
+                .collect(Collectors.toMap(FmpStockData::getSymbol, Function.identity(), (first, second) -> first));
+
+        Map<String, FmpVolumeData> volumeDataMap = Arrays
+                .stream(Exchange.values())
+                .flatMap(exchange -> fetchVolumeList(exchange).stream())
+                .collect(Collectors.toMap(FmpVolumeData::symbol, fmpVolumeData -> fmpVolumeData));
+
+        return stockDataMap.entrySet()
+                .stream()
+                .map(entry -> {
+                    String tickerName = entry.getKey();
+                    FmpStockData fmpStockData = entry.getValue();
+                    FmpVolumeData fmpVolumeData = volumeDataMap
+                            .getOrDefault(tickerName, new FmpVolumeData(tickerName, null, null));
+
+                    return new StockData(
+                            tickerName,
+                            fmpStockData.getCompanyName(),
+                            fmpStockData.getExchangeShortName(),
+                            Sector.fromName(fmpStockData.getSector()),
+                            fmpStockData.getIndustry(),
+                            fmpStockData.getPrice(),
+                            fmpVolumeData.volume(),
+                            fmpVolumeData.avgVolume()
+                    );
+                })
+                .toList();
+    }
+
+    private Stream<FmpStockData> fetchStockList(final String sector) {
+        return fmpWebClient.get()
+                .uri(uriBuilder -> uriBuilder
+                        .path(fmpProperties.getStockScreenerPath())
+                        .queryParam("apikey", fmpProperties.getApiKey())
+                        .queryParam("exchange", Exchange.getNames())
+                        .queryParam("sector", sector)
+                        .queryParam("limit", MAX_LIMIT)
+                        .build())
+                .retrieve()
+                .bodyToFlux(FmpStockData.class)
+                .collectList()
+                .block()
+                .stream();
+    }
+
+    private Stream<FmpStockData> fetchEtfStockList() {
+        return fmpWebClient.get()
+                .uri(uriBuilder -> uriBuilder
+                        .path(fmpProperties.getStockScreenerPath())
+                        .queryParam("apikey", fmpProperties.getApiKey())
+                        .queryParam("isEtf", true)
+                        .build())
+                .retrieve()
+                .bodyToFlux(FmpStockData.class)
+                .map(fmpStockData -> {
+                    fmpStockData.setSector(ETF.getName());
+                    return fmpStockData;
+                })
+                .collectList()
+                .block()
+                .stream();
+    }
+
+    private List<FmpVolumeData> fetchVolumeList(final Exchange exchange) {
+        return fmpWebClient.get()
+                .uri(uriBuilder -> uriBuilder
+                        .path(fmpProperties.getExchangeSymbolsStockListPath() + exchange.name())
+                        .queryParam("apikey", fmpProperties.getApiKey())
+                        .build())
+                .retrieve()
+                .bodyToFlux(FmpVolumeData.class)
+                .collectList()
+                .block();
+    }
+
+    @Override
+    public List<DividendData> getPastDividendList() {
+
+        // 현재 시간을 기준으로 작년 1월 ~ 12월의 배당금 데이터를 조회
+        List<DividendData> result = new ArrayList<>();
+        for (int month = 12; month >= 3; month -= 3) {
+
+            Instant date = LocalDate.of(
+                            InstantProvider.getLastYear(),
+                            month,
+                            1)
+                    .atStartOfDay()
+                    .toInstant(UTC);
+
+            List<DividendData> dividendResponses = fetchDividendList(date)
+                    .stream()
+                    .map(FmpDividendData::toDividendData)
+                    .toList();
+
+            if (dividendResponses.isEmpty()) {
+                log.error("FmpClient updateDividendData 수행 중 에러 발생: dividendResponses is empty");
+                continue;
+            }
+
+            result.addAll(dividendResponses);
+        }
+
+        return result;
+    }
+
+    @Override
+    public List<DividendData> getUpcomingDividendList() {
+
+        List<DividendData> dividendResponse = fetchDividendList(
+                LocalDate.now().atStartOfDay().toInstant(UTC),
+                LocalDate.now().plusMonths(3).atStartOfDay().toInstant(UTC)
+        )
+                .stream()
+                .map(FmpDividendData::toDividendData)
+                .toList();
+
+        if (dividendResponse.isEmpty()) {
+            log.error("FmpClient updateDividendData 수행 중 에러 발생: dividendResponses is empty");
+        }
+
+        return dividendResponse;
+    }
+
+    private List<FmpDividendData> fetchDividendList(Instant date) {
+        return fmpWebClient.get()
+                .uri(uriBuilder ->
+                        uriBuilder
+                                .path(fmpProperties.getStockDividendCalenderPath())
+                                .queryParam("to", DateFormat.formatInstant(date))
+                                .queryParam("apikey", fmpProperties.getApiKey())
+                                .build())
+                .retrieve()
+                .bodyToFlux(FmpDividendData.class)
+                .onErrorResume(throwable -> {
+                    log.error("FmpClient updateDividendData 수행 중 에러 발생: {}", throwable.getMessage());
+                    return Mono.empty();
+                })
+                .collectList()
+                .block();
+    }
+
+    private List<FmpDividendData> fetchDividendList(Instant from, Instant to) {
+        return fmpWebClient.get()
+                .uri(uriBuilder ->
+                        uriBuilder
+                                .path(fmpProperties.getStockDividendCalenderPath())
+                                .queryParam("from", DateFormat.formatInstant(from))
+                                .queryParam("to", DateFormat.formatInstant(to))
+                                .queryParam("apikey", fmpProperties.getApiKey())
+                                .build())
+                .retrieve()
+                .bodyToFlux(FmpDividendData.class)
+                .onErrorResume(throwable -> {
+                    log.error("FmpClient updateDividendData 수행 중 에러 발생: {}", throwable.getMessage());
+                    return Mono.empty();
+                })
+                .collectList()
+                .block();
+    }
+}
diff --git a/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpProperties.java b/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpProperties.java
new file mode 100644
index 00000000..61e62920
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/infra/fmp/FmpProperties.java
@@ -0,0 +1,17 @@
+package nexters.payout.batch.infra.fmp;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("financial.fmp")
+@RequiredArgsConstructor
+@Getter
+public class FmpProperties {
+    final String apiKey;
+    final String baseUrl;
+    final String stockListPath;
+    final String stockScreenerPath;
+    final String exchangeSymbolsStockListPath;
+    final String stockDividendCalenderPath;
+}
diff --git a/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasDto.java b/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasDto.java
new file mode 100644
index 00000000..804376b0
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasDto.java
@@ -0,0 +1,8 @@
+package nexters.payout.batch.infra.ninjas;
+
+record NinjasStockLogo(
+        String name,
+        String ticker,
+        String image
+) {
+}
\ No newline at end of file
diff --git a/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasFinancialClient.java b/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasFinancialClient.java
new file mode 100644
index 00000000..b4c46b27
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasFinancialClient.java
@@ -0,0 +1,42 @@
+package nexters.payout.batch.infra.ninjas;
+
+import lombok.extern.slf4j.Slf4j;
+import nexters.payout.batch.application.StockLogo;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+
+@Service
+@Slf4j
+public class NinjasFinancialClient implements StockLogo {
+
+    private final WebClient ninjasWebClient;
+    private final NinjasProperties ninjasProperties;
+
+    NinjasFinancialClient(final NinjasProperties ninjasProperties) {
+        this.ninjasProperties = ninjasProperties;
+        this.ninjasWebClient = WebClient.builder()
+                .baseUrl(ninjasProperties.getBaseUrl())
+                .defaultHeader("X-Api-Key", ninjasProperties.getApiKey())
+                .build();
+    }
+
+    @Override
+    public String getLogoUrl(String ticker) {
+        return fetchLogoUrl(ticker).image();
+    }
+
+    private NinjasStockLogo fetchLogoUrl(String ticker) {
+        return ninjasWebClient.get()
+                .uri(uriBuilder -> uriBuilder
+                        .path(ninjasProperties.getLogoPath())
+                        .queryParam("ticker", ticker)
+                        .build())
+                .retrieve()
+                .bodyToFlux(NinjasStockLogo.class)
+                .next()
+                .doOnError(e -> log.error("fetchLogoUrl 호출 실패: {}", e.getMessage()))
+                .onErrorReturn(new NinjasStockLogo(ticker, ticker, null))
+                .blockOptional()
+                .orElse(new NinjasStockLogo(ticker, ticker, null));
+    }
+}
diff --git a/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasProperties.java b/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasProperties.java
new file mode 100644
index 00000000..44fc41e9
--- /dev/null
+++ b/batch/src/main/java/nexters/payout/batch/infra/ninjas/NinjasProperties.java
@@ -0,0 +1,14 @@
+package nexters.payout.batch.infra.ninjas;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("financial.ninjas")
+@RequiredArgsConstructor
+@Getter
+public class NinjasProperties {
+    final String apiKey;
+    final String baseUrl;
+    final String logoPath;
+}
diff --git a/batch/src/main/resources/application-dev.yml b/batch/src/main/resources/application-dev.yml
new file mode 100644
index 00000000..980b9ec1
--- /dev/null
+++ b/batch/src/main/resources/application-dev.yml
@@ -0,0 +1,33 @@
+spring:
+  datasource:
+    url: jdbc:mysql://localhost:3306/nexters
+    username: test
+    password: test
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: validate
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: true
+
+financial:
+  fmp:
+    api-key: ${FMP_API_KEY}
+    base-url: https://financialmodelingprep.com
+    stock-list-path: /api/v3/stock/list
+    exchange-symbols-stock-list-path: /api/v3/symbol/
+    stock-screener-path: /api/v3/stock-screener
+    stock-dividend-calender-path: /api/v3/stock_dividend_calendar
+  ninjas:
+    api-key: ${NINJAS_API_KEY}
+    base-url: https://api.api-ninjas.com
+    logo-path: /v1/logo
+
+schedules:
+  cron:
+    stock: "0 0 3 * * *"
+    dividend:
+      past: "0 0 4 * * 0"
+      future: "0 0 4 * * *"
diff --git a/batch/src/main/resources/application-prod.yml b/batch/src/main/resources/application-prod.yml
new file mode 100644
index 00000000..96241081
--- /dev/null
+++ b/batch/src/main/resources/application-prod.yml
@@ -0,0 +1,43 @@
+spring:
+  datasource:
+    url: jdbc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE}
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: update
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: false
+
+  flyway:
+    enabled: true
+    baseline-on-migrate: true
+    url: jdbc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE}
+    user: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+    baseline-version: 0
+
+schedules:
+  cron:
+    stock: "0 2 * * * *"
+    dividend:
+      past: "0 4 * * * 0"
+      future: "0 4 * * * *"
+
+financial:
+  fmp:
+    api-key: ${FMP_API_KEY}
+    base-url: https://financialmodelingprep.com
+    stock-list-path: /api/v3/stock/list
+    exchange-symbols-stock-list-path: /api/v3/symbol/
+    stock-screener-path: /api/v3/stock-screener
+    stock-dividend-calender-path: /api/v3/stock_dividend_calendar
+  ninjas:
+    api-key: ${NINJAS_API_KEY}
+    base-url: https://api.api-ninjas.com
+    logo-path: /v1/logo
\ No newline at end of file
diff --git a/batch/src/main/resources/application-test.yml b/batch/src/main/resources/application-test.yml
new file mode 100644
index 00000000..7c3de5e6
--- /dev/null
+++ b/batch/src/main/resources/application-test.yml
@@ -0,0 +1,22 @@
+spring:
+  datasource:
+    driver-class-name: org.h2.Driver
+    url: jdbc:h2:mem:test;MODE=MySQL
+    username: sa
+    password:
+
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: create-drop
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: true
+
+schedules:
+  cron:
+    stock: "-"
+    dividend:
+      past: "-"
+      future: "-"
\ No newline at end of file
diff --git a/batch/src/main/resources/application.properties b/batch/src/main/resources/application.properties
deleted file mode 100644
index 8b137891..00000000
--- a/batch/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/batch/src/main/resources/application.yml b/batch/src/main/resources/application.yml
new file mode 100644
index 00000000..76d483dd
--- /dev/null
+++ b/batch/src/main/resources/application.yml
@@ -0,0 +1,5 @@
+spring:
+  profiles:
+    active: dev
+
+
diff --git a/batch/src/test/java/nexters/dividend/batch/DividendBatchApplicationTests.java b/batch/src/test/java/nexters/dividend/batch/DividendBatchApplicationTests.java
deleted file mode 100644
index 61ed9250..00000000
--- a/batch/src/test/java/nexters/dividend/batch/DividendBatchApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package nexters.dividend.batch;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class DividendBatchApplicationTests {
-
-    @Test
-    void contextLoads() {
-    }
-
-}
diff --git a/batch/src/test/java/nexters/payout/batch/PayoutBatchApplicationTests.java b/batch/src/test/java/nexters/payout/batch/PayoutBatchApplicationTests.java
new file mode 100644
index 00000000..4a2fb959
--- /dev/null
+++ b/batch/src/test/java/nexters/payout/batch/PayoutBatchApplicationTests.java
@@ -0,0 +1,15 @@
+package nexters.payout.batch;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.TestPropertySource;
+
+@SpringBootTest
+@TestPropertySource(properties = { "spring.config.location=classpath:application-test.yml" })
+class PayoutBatchApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}
diff --git a/batch/src/test/java/nexters/payout/batch/application/DividendBatchServiceTest.java b/batch/src/test/java/nexters/payout/batch/application/DividendBatchServiceTest.java
new file mode 100644
index 00000000..9568a2ca
--- /dev/null
+++ b/batch/src/test/java/nexters/payout/batch/application/DividendBatchServiceTest.java
@@ -0,0 +1,186 @@
+package nexters.payout.batch.application;
+
+import nexters.payout.batch.common.AbstractBatchServiceTest;
+import nexters.payout.domain.DividendFixture;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockitoAnnotations;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.test.mock.mockito.SpyBean;
+import org.springframework.data.auditing.AuditingHandler;
+import org.springframework.data.auditing.DateTimeProvider;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static java.time.ZoneOffset.UTC;
+import static nexters.payout.core.time.InstantProvider.*;
+import static nexters.payout.domain.StockFixture.AAPL;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.BDDMockito.given;
+
+@DisplayName("배당금 스케쥴러 서비스 테스트")
+class DividendBatchServiceTest extends AbstractBatchServiceTest {
+
+    @MockBean
+    DateTimeProvider dateTimeProvider;
+
+    @SpyBean
+    AuditingHandler auditingHandler;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        auditingHandler.setDateTimeProvider(dateTimeProvider);
+    }
+
+    @Test
+    void 새로운_과거_배당금_정보를_생성한다() {
+
+        // given
+        Stock stock = stockRepository.save(StockFixture.createStock(AAPL, 12.51, 120000));
+        Dividend expected = DividendFixture.createDividend(stock.getId());
+
+        List<FinancialClient.DividendData> responses = new ArrayList<>();
+        responses.add(new FinancialClient.DividendData(
+                Instant.parse("2023-12-21T00:00:00Z"),
+                "May 31, 23",
+                12.21,
+                "AAPL",
+                12.21,
+                Instant.parse("2023-12-21T00:00:00Z"),
+                Instant.parse("2023-12-23T00:00:00Z"),
+                Instant.parse("2023-12-22T00:00:00Z")));
+
+        given(financialClient.getPastDividendList()).willReturn(responses);
+
+        // when
+        dividendBatchService.updatePastDividendInfo();
+
+        // then
+        assertThat(dividendRepository.findByStockIdAndExDividendDate(
+                stock.getId(),
+                Instant.parse("2023-12-21T00:00:00Z")))
+                .isPresent();
+
+        Dividend actual = dividendRepository.findByStockIdAndExDividendDate(
+                        stock.getId(),
+                        Instant.parse("2023-12-21T00:00:00Z"))
+                .get();
+
+        assertAll(
+                () -> assertThat(actual.getDividend()).isEqualTo(expected.getDividend()),
+                () -> assertThat(actual.getExDividendDate()).isEqualTo(expected.getExDividendDate()),
+                () -> assertThat(dividendRepository.findAll().size()).isEqualTo(1)
+        );
+    }
+
+    @Test
+    void 기존의_과거_배당금_정보를_갱신한다() {
+
+        // given
+        Stock stock = stockRepository.save(StockFixture.createStock(AAPL, 12.51, 120000));
+        Dividend expected = dividendRepository.save(DividendFixture.createDividendWithNullDate(stock.getId()));
+
+        List<FinancialClient.DividendData> responses = new ArrayList<>();
+        responses.add(new FinancialClient.DividendData(
+                Instant.parse("2023-12-21T00:00:00Z"),
+                "May 31, 23",
+                12.21,
+                AAPL,
+                12.21,
+                Instant.parse("2023-12-21T00:00:00Z"),
+                Instant.parse("2023-12-23T00:00:00Z"),
+                Instant.parse("2023-12-22T00:00:00Z")));
+
+        given(financialClient.getPastDividendList()).willReturn(responses);
+
+        // when
+        dividendBatchService.updatePastDividendInfo();
+
+        // then
+        Dividend actual = dividendRepository.findByStockIdAndExDividendDate(
+                        stock.getId(),
+                        Instant.parse("2023-12-21T00:00:00Z"))
+                .get();
+
+        assertAll(
+                () -> assertThat(actual.getDividend()).isEqualTo(expected.getDividend()),
+                () -> assertThat(actual.getExDividendDate()).isEqualTo(expected.getExDividendDate()),
+                () -> assertThat(actual.getPaymentDate()).isEqualTo(Instant.parse("2023-12-23T00:00:00Z")),
+                () -> assertThat(actual.getDeclarationDate()).isEqualTo(Instant.parse("2023-12-22T00:00:00Z")),
+                () -> assertThat(dividendRepository.findAll().size()).isEqualTo(1)
+        );
+    }
+
+    @Test
+    void 미래_배당금_정보를_생성할때_어제_삽입된_미래_배당금_정보는_제거된다() {
+        // given
+        given(dateTimeProvider.getNow()).willReturn(Optional.of(LocalDateTime.now().minusDays(1)));
+        Stock stock = stockRepository.save(StockFixture.createStock(AAPL, 12.51, 120000));
+        dividendRepository.save(DividendFixture.createDividend(
+                stock.getId(),
+                21.02,
+                LocalDateTime.now().toInstant(UTC)));
+
+        given(financialClient.getUpcomingDividendList()).willReturn(new ArrayList<>());
+
+        // when
+        given(dateTimeProvider.getNow()).willReturn(Optional.of(LocalDateTime.now()));
+        dividendBatchService.updateUpcomingDividendInfo();
+
+        // then
+        assertThat(dividendRepository.count()).isEqualTo(0);
+    }
+
+    @Test
+    void 새로운_미래_배당금_정보를_생성한다() {
+        // given
+        Stock stock = stockRepository.save(StockFixture.createStock(AAPL, 12.51, 120000));
+        Dividend expected = DividendFixture.createDividend(stock.getId());
+        Instant expectedDate = LocalDateTime.now().plusDays(1).toInstant(UTC);
+
+        List<FinancialClient.DividendData> responses = new ArrayList<>();
+        responses.add(new FinancialClient.DividendData(
+                expectedDate,
+                "May 31, 23",
+                12.21,
+                AAPL,
+                12.21,
+                expectedDate,
+                expectedDate,
+                expectedDate));
+
+        given(financialClient.getUpcomingDividendList()).willReturn(responses);
+
+        // when
+        dividendBatchService.updateUpcomingDividendInfo();
+
+        // then
+        assertThat(dividendRepository.findByStockIdAndExDividendDate(
+                stock.getId(),
+                expectedDate))
+                .isPresent();
+
+        Dividend actual = dividendRepository.findByStockIdAndExDividendDate(
+                        stock.getId(),
+                        expectedDate)
+                .get();
+
+        assertAll(
+                () -> assertThat(actual.getDividend()).isEqualTo(expected.getDividend()),
+                () -> assertThat(getYear(actual.getExDividendDate())).isEqualTo(getYear(expectedDate)),
+                () -> assertThat(getMonth(actual.getExDividendDate())).isEqualTo(getMonth(expectedDate)),
+                () -> assertThat(getDayOfMonth(actual.getExDividendDate())).isEqualTo(getDayOfMonth(expectedDate)),
+                () -> assertThat(dividendRepository.findAll().size()).isEqualTo(1)
+        );
+    }
+}
\ No newline at end of file
diff --git a/batch/src/test/java/nexters/payout/batch/application/LatestStockFixture.java b/batch/src/test/java/nexters/payout/batch/application/LatestStockFixture.java
new file mode 100644
index 00000000..9b6e95d8
--- /dev/null
+++ b/batch/src/test/java/nexters/payout/batch/application/LatestStockFixture.java
@@ -0,0 +1,11 @@
+package nexters.payout.batch.application;
+
+import nexters.payout.domain.stock.domain.Exchange;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.batch.application.FinancialClient.StockData;
+
+public class LatestStockFixture {
+    public static StockData createStockData(String ticker, Double price, Integer volume) {
+        return new StockData(ticker, ticker, Exchange.AMEX.name(), Sector.FINANCIAL_SERVICES, "industry", price, volume, volume);
+    }
+}
diff --git a/batch/src/test/java/nexters/payout/batch/application/StockBatchServiceTest.java b/batch/src/test/java/nexters/payout/batch/application/StockBatchServiceTest.java
new file mode 100644
index 00000000..c452af08
--- /dev/null
+++ b/batch/src/test/java/nexters/payout/batch/application/StockBatchServiceTest.java
@@ -0,0 +1,35 @@
+package nexters.payout.batch.application;
+
+import nexters.payout.batch.common.AbstractBatchServiceTest;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.stock.domain.Stock;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.mockito.BDDMockito.given;
+
+@DisplayName("주식 스케쥴러 서비스 테스트")
+class StockBatchServiceTest extends AbstractBatchServiceTest {
+
+    @Test
+    void 현재가와_거래량을_업데이트한다() {
+        // given
+        Stock stock = stockRepository.save(StockFixture.createStock(StockFixture.TSLA, 10.0, 1234));
+        FinancialClient.StockData stockData = LatestStockFixture.createStockData(stock.getTicker(), 30.0, 4321);
+        given(financialClient.getLatestStockList()).willReturn(List.of(stockData));
+
+        // when
+        stockBatchService.run();
+
+        // then
+        Stock actual = stockRepository.findByTicker(stock.getTicker()).get();
+        assertAll(
+                () -> assertThat(actual.getPrice()).isEqualTo(stockData.price()),
+                () -> assertThat(actual.getVolume()).isEqualTo(stockData.volume())
+        );
+    }
+}
\ No newline at end of file
diff --git a/batch/src/test/java/nexters/payout/batch/common/AbstractBatchServiceTest.java b/batch/src/test/java/nexters/payout/batch/common/AbstractBatchServiceTest.java
new file mode 100644
index 00000000..dde3c178
--- /dev/null
+++ b/batch/src/test/java/nexters/payout/batch/common/AbstractBatchServiceTest.java
@@ -0,0 +1,37 @@
+package nexters.payout.batch.common;
+
+import nexters.payout.batch.application.DividendBatchService;
+import nexters.payout.batch.application.FinancialClient;
+import nexters.payout.batch.application.StockBatchService;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.junit.jupiter.api.AfterEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest
+@ActiveProfiles("test")
+public abstract class AbstractBatchServiceTest {
+    @MockBean
+    public FinancialClient financialClient;
+
+    @Autowired
+    public StockRepository stockRepository;
+
+    @Autowired
+    public DividendRepository dividendRepository;
+
+    @Autowired
+    public StockBatchService stockBatchService;
+
+    @Autowired
+    public DividendBatchService dividendBatchService;
+
+    @AfterEach
+    void afterEach() {
+        dividendRepository.deleteAll();
+        stockRepository.deleteAll();
+    }
+}
diff --git a/build.gradle b/build.gradle
index 4f56a3b9..d2399f83 100644
--- a/build.gradle
+++ b/build.gradle
@@ -11,4 +11,4 @@ allprojects {
 
 jar {
     enabled = false
-}
\ No newline at end of file
+}
diff --git a/core/build.gradle b/core/build.gradle
index 99a76661..dc480752 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -10,6 +10,8 @@ jar.enabled = true
 dependencies {
     implementation 'org.springframework.boot:spring-boot-starter-web'
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
+    // Spring Web
+    implementation 'org.springframework.boot:spring-boot-starter-web'
 }
 
 tasks.named('test') {
diff --git a/core/src/main/java/nexters/dividend/core/DividendCoreApplication.java b/core/src/main/java/nexters/payout/core/PayoutCoreApplication.java
similarity index 59%
rename from core/src/main/java/nexters/dividend/core/DividendCoreApplication.java
rename to core/src/main/java/nexters/payout/core/PayoutCoreApplication.java
index 7622dfc1..c28a9b61 100644
--- a/core/src/main/java/nexters/dividend/core/DividendCoreApplication.java
+++ b/core/src/main/java/nexters/payout/core/PayoutCoreApplication.java
@@ -1,13 +1,13 @@
-package nexters.dividend.core;
+package nexters.payout.core;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 @SpringBootApplication
-public class DividendCoreApplication {
+public class PayoutCoreApplication {
 
     public static void main(String[] args) {
-        SpringApplication.run(DividendCoreApplication.class, args);
+        SpringApplication.run(PayoutCoreApplication.class, args);
     }
 
 }
diff --git a/core/src/main/java/nexters/dividend/core/exception/ErrorResponse.java b/core/src/main/java/nexters/payout/core/exception/ErrorResponse.java
similarity index 64%
rename from core/src/main/java/nexters/dividend/core/exception/ErrorResponse.java
rename to core/src/main/java/nexters/payout/core/exception/ErrorResponse.java
index d2e1ff00..1381cc3b 100644
--- a/core/src/main/java/nexters/dividend/core/exception/ErrorResponse.java
+++ b/core/src/main/java/nexters/payout/core/exception/ErrorResponse.java
@@ -1,4 +1,4 @@
-package nexters.dividend.core.exception;
+package nexters.payout.core.exception;
 
 public record ErrorResponse(
         int code,
diff --git a/core/src/main/java/nexters/dividend/core/exception/GlobalExceptionHandler.java b/core/src/main/java/nexters/payout/core/exception/GlobalExceptionHandler.java
similarity index 83%
rename from core/src/main/java/nexters/dividend/core/exception/GlobalExceptionHandler.java
rename to core/src/main/java/nexters/payout/core/exception/GlobalExceptionHandler.java
index adcd078f..df5a59cc 100644
--- a/core/src/main/java/nexters/dividend/core/exception/GlobalExceptionHandler.java
+++ b/core/src/main/java/nexters/payout/core/exception/GlobalExceptionHandler.java
@@ -1,8 +1,8 @@
-package nexters.dividend.core.exception;
+package nexters.payout.core.exception;
 
-import nexters.dividend.core.exception.error.AlreadyExistsException;
-import nexters.dividend.core.exception.error.BadRequestException;
-import nexters.dividend.core.exception.error.NotFoundException;
+import nexters.payout.core.exception.error.AlreadyExistsException;
+import nexters.payout.core.exception.error.BadRequestException;
+import nexters.payout.core.exception.error.NotFoundException;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatusCode;
@@ -13,6 +13,7 @@
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 import org.springframework.web.context.request.WebRequest;
+import org.springframework.web.method.annotation.HandlerMethodValidationException;
 import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
 
 import java.util.NoSuchElementException;
@@ -20,6 +21,17 @@
 @RestControllerAdvice
 public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
 
+    @Override
+    protected ResponseEntity<Object> handleHandlerMethodValidationException(
+            HandlerMethodValidationException ex,
+            HttpHeaders headers,
+            HttpStatusCode status,
+            WebRequest request) {
+
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), ex.getMessage()));
+    }
+
     @Override
     protected ResponseEntity<Object> handleMethodArgumentNotValid(
             final MethodArgumentNotValidException ex,
diff --git a/core/src/main/java/nexters/dividend/core/exception/error/AlreadyExistsException.java b/core/src/main/java/nexters/payout/core/exception/error/AlreadyExistsException.java
similarity index 76%
rename from core/src/main/java/nexters/dividend/core/exception/error/AlreadyExistsException.java
rename to core/src/main/java/nexters/payout/core/exception/error/AlreadyExistsException.java
index 0ef83ef6..ca94db27 100644
--- a/core/src/main/java/nexters/dividend/core/exception/error/AlreadyExistsException.java
+++ b/core/src/main/java/nexters/payout/core/exception/error/AlreadyExistsException.java
@@ -1,4 +1,4 @@
-package nexters.dividend.core.exception.error;
+package nexters.payout.core.exception.error;
 
 public class AlreadyExistsException extends BaseException {
     public AlreadyExistsException(final String message) {
diff --git a/core/src/main/java/nexters/dividend/core/exception/error/BadRequestException.java b/core/src/main/java/nexters/payout/core/exception/error/BadRequestException.java
similarity index 75%
rename from core/src/main/java/nexters/dividend/core/exception/error/BadRequestException.java
rename to core/src/main/java/nexters/payout/core/exception/error/BadRequestException.java
index 80a1b8e7..6c8b3b30 100644
--- a/core/src/main/java/nexters/dividend/core/exception/error/BadRequestException.java
+++ b/core/src/main/java/nexters/payout/core/exception/error/BadRequestException.java
@@ -1,4 +1,4 @@
-package nexters.dividend.core.exception.error;
+package nexters.payout.core.exception.error;
 
 public class BadRequestException extends BaseException {
     public BadRequestException(final String message) {
diff --git a/core/src/main/java/nexters/dividend/core/exception/error/BaseException.java b/core/src/main/java/nexters/payout/core/exception/error/BaseException.java
similarity index 84%
rename from core/src/main/java/nexters/dividend/core/exception/error/BaseException.java
rename to core/src/main/java/nexters/payout/core/exception/error/BaseException.java
index 75a43773..d2b0c6d6 100644
--- a/core/src/main/java/nexters/dividend/core/exception/error/BaseException.java
+++ b/core/src/main/java/nexters/payout/core/exception/error/BaseException.java
@@ -1,4 +1,4 @@
-package nexters.dividend.core.exception.error;
+package nexters.payout.core.exception.error;
 
 public class BaseException extends RuntimeException {
     private final String message;
diff --git a/core/src/main/java/nexters/dividend/core/exception/error/NotFoundException.java b/core/src/main/java/nexters/payout/core/exception/error/NotFoundException.java
similarity index 75%
rename from core/src/main/java/nexters/dividend/core/exception/error/NotFoundException.java
rename to core/src/main/java/nexters/payout/core/exception/error/NotFoundException.java
index 8cdb366d..2d512dbd 100644
--- a/core/src/main/java/nexters/dividend/core/exception/error/NotFoundException.java
+++ b/core/src/main/java/nexters/payout/core/exception/error/NotFoundException.java
@@ -1,4 +1,4 @@
-package nexters.dividend.core.exception.error;
+package nexters.payout.core.exception.error;
 
 public class NotFoundException extends BaseException {
     public NotFoundException(final String message) {
diff --git a/core/src/main/java/nexters/payout/core/time/DateFormat.java b/core/src/main/java/nexters/payout/core/time/DateFormat.java
new file mode 100644
index 00000000..81a5a916
--- /dev/null
+++ b/core/src/main/java/nexters/payout/core/time/DateFormat.java
@@ -0,0 +1,25 @@
+package nexters.payout.core.time;
+
+import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.util.Date;
+
+public class DateFormat {
+    /**
+     * "yyyy-MM-dd" 형식의 String을 Instant 타입으로 변환합니다.
+     */
+    public static Instant parseInstant(final String date) {
+
+        if (date == null) return null;
+        return Instant.parse(date + "T00:00:00Z");
+    }
+
+    /**
+     * Instant를 "yyyy-MM-dd" 형식의 String으로 변환합니다.
+     */
+    public static String formatInstant(Instant instant) {
+
+        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+        return formatter.format(Date.from(instant));
+    }
+}
diff --git a/core/src/main/java/nexters/payout/core/time/InstantProvider.java b/core/src/main/java/nexters/payout/core/time/InstantProvider.java
new file mode 100644
index 00000000..fcbfb307
--- /dev/null
+++ b/core/src/main/java/nexters/payout/core/time/InstantProvider.java
@@ -0,0 +1,64 @@
+package nexters.payout.core.time;
+
+import java.time.*;
+import java.time.temporal.ChronoUnit;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.time.ZoneOffset.UTC;
+
+public class InstantProvider {
+    public static LocalDate toLocalDate(Instant instant) {
+        return LocalDate.ofInstant(instant, UTC);
+    }
+
+    public static List<YearMonth> generateNext12Months() {
+        YearMonth startYearMonth = getThisYearMonth();
+        YearMonth endYearMonth = getAfterYearMonth(11);
+
+        return Stream.iterate(startYearMonth, date -> date.plusMonths(1))
+                .limit(startYearMonth.until(endYearMonth, ChronoUnit.MONTHS) + 1)
+                .collect(Collectors.toList());
+    }
+
+    public static YearMonth getThisYearMonth() {
+        return YearMonth.of(getNow().getYear(), getNow().getMonth());
+    }
+
+    public static YearMonth getAfterYearMonth(int month) {
+        return YearMonth.of(getNow().plusMonths(month).getYear(), getNow().plusMonths(month).getMonthValue());
+    }
+
+    public static Integer getThisYear() {
+        return getNow().getYear();
+    }
+
+    public static Integer getNextYear() {
+        return getNow().plusYears(1).getYear();
+    }
+
+    public static Integer getLastYear() {
+        return getNow().minusYears(1).getYear();
+    }
+
+    public static Instant getYesterday() {
+        return getNow().minusDays(1).atStartOfDay(ZoneId.of("UTC")).toInstant();
+    }
+
+    public static Integer getYear(Instant date) {
+        return ZonedDateTime.ofInstant(date, UTC).getYear();
+    }
+
+    public static Integer getMonth(Instant date) {
+        return ZonedDateTime.ofInstant(date, UTC).getMonthValue();
+    }
+
+    public static Integer getDayOfMonth(Instant date) {
+        return ZonedDateTime.ofInstant(date, UTC).getDayOfMonth();
+    }
+
+    public static LocalDate getNow() {
+        return LocalDate.ofInstant(Instant.now(), UTC);
+    }
+}
diff --git a/core/src/main/resources/logback-spring.xml b/core/src/main/resources/logback-spring.xml
index a1923d19..e939157f 100644
--- a/core/src/main/resources/logback-spring.xml
+++ b/core/src/main/resources/logback-spring.xml
@@ -1,11 +1,10 @@
 <configuration>
-    <springProfile name="console-logging">
-        <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
-        <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
-        <root level="INFO">
-            <appender-ref ref="CONSOLE"/>
-        </root>
-    </springProfile>
+
+    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
+    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
+    <root level="INFO">
+        <appender-ref ref="CONSOLE"/>
+    </root>
 
     <springProfile name="file-logging">
         <property name="LOG_PATH" value="./logs"/>
@@ -16,10 +15,10 @@
             <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                 <fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}-info-%i.log</fileNamePattern>
                 <maxFileSize>50MB</maxFileSize>
-                <maxHistory>30</maxHistory>
+                <maxHistory>3</maxHistory>
             </rollingPolicy>
             <encoder>
-                <pattern>%d{HH:mm:ss.SSS} [%level] [%thread] [%logger{36}] - %msg%n</pattern>
+                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%level] [%thread] [%logger{36}] - %msg%n</pattern>
             </encoder>
         </appender>
         <root level="INFO">
diff --git a/core/src/test/java/nexters/dividend/core/DividendCoreApplicationTests.java b/core/src/test/java/nexters/payout/core/PayoutCoreApplicationTests.java
similarity index 70%
rename from core/src/test/java/nexters/dividend/core/DividendCoreApplicationTests.java
rename to core/src/test/java/nexters/payout/core/PayoutCoreApplicationTests.java
index 41471b56..525fc157 100644
--- a/core/src/test/java/nexters/dividend/core/DividendCoreApplicationTests.java
+++ b/core/src/test/java/nexters/payout/core/PayoutCoreApplicationTests.java
@@ -1,10 +1,10 @@
-package nexters.dividend.core;
+package nexters.payout.core;
 
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 
 @SpringBootTest
-class DividendCoreApplicationTests {
+class PayoutCoreApplicationTests {
 
     @Test
     void contextLoads() {
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 00000000..7a1a9d1d
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,60 @@
+#!/bin/bash
+
+# Docker install
+if ! command -v docker &> /dev/null; then
+    echo "Docker is not installed..."
+    echo "Docker install start..."
+    sudo apt-get update -y
+    sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common
+    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+    sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
+    sudo apt-get update -y
+    sudo apt-get install -y docker-ce docker-ce-cli containerd.io
+    echo "Docker install complete"
+else
+    echo "Docker is already installed"
+fi
+
+# Docker-compose install
+if ! command -v docker-compose &> /dev/null; then
+    echo "Docker-compose is not installed..."
+    echo "Docker-compose install start..."
+    sudo curl -L "https://github.com/docker/compose/releases/download/1.28.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+    sudo chmod +x /usr/local/bin/docker-compose
+    sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
+    echo "Docker-compose install complete!"
+else
+    echo "Docker-compose is already installed"
+fi
+
+RUNNING_CONTAINER=$(docker ps | grep blue)
+NGINX_CONF="/home/nginx.conf"
+RUNNING_NGINX=$(docker ps | grep nginx)
+BATCH_CONTAINER="batch"
+
+
+if [ -z "$RUNNING_CONTAINER" ]; then
+    TARGET_SERVICE="blue-api"
+    OTHER_SERVICE="green-api"
+else
+    TARGET_SERVICE="green-api"
+    OTHER_SERVICE="blue-api"
+fi
+
+echo "$TARGET_SERVICE Deploy..."
+docker-compose -f /home/docker-compose.yml up -d $TARGET_SERVICE $BATCH_CONTAINER
+
+# Wait for the target service to be healthy before proceeding
+sleep 10
+
+if [ -z "$RUNNING_NGINX" ]; then
+    echo "Starting Nginx..."
+    docker-compose -f /home/docker-compose.yml up -d nginx
+fi
+
+# Update the nginx config and reload
+sed -it "s/$OTHER_SERVICE/$TARGET_SERVICE/" $NGINX_CONF
+docker-compose -f /home/docker-compose.yml restart nginx
+
+# Stop the other service
+docker-compose -f /home/docker-compose.yml stop $OTHER_SERVICE
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..43ffdbd8
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,82 @@
+version: '3'
+
+services:
+
+  nginx:
+    container_name: nginx
+    image: nginx:latest
+    ports:
+      - "80:80"
+    volumes:
+      - /home/nginx.conf:/etc/nginx/nginx.conf
+    depends_on:
+      - "db"
+    restart: always
+
+  blue-api:
+    container_name: blue-api
+    depends_on:
+      - db
+    image: ${NCP_CONTAINER_REGISTRY_API}/payout-api
+    expose:
+      - "8080"
+    environment:
+      DB_HOSTNAME: ${DB_HOSTNAME}
+      DB_PORT: ${DB_PORT}
+      DB_DATABASE: ${DB_DATABASE}
+      DB_USERNAME: ${DB_USERNAME}
+      DB_PASSWORD: ${DB_PASSWORD}
+      FMP_API_KEY: ${FMP_API_KEY}
+    restart: always
+    volumes:
+      - ./logs/api-server:/logs
+
+  green-api:
+    container_name: green-api
+    depends_on:
+      - db
+    image: ${NCP_CONTAINER_REGISTRY_API}/payout-api
+    expose:
+      - "8080"
+    environment:
+      DB_HOSTNAME: ${DB_HOSTNAME}
+      DB_PORT: ${DB_PORT}
+      DB_DATABASE: ${DB_DATABASE}
+      DB_USERNAME: ${DB_USERNAME}
+      DB_PASSWORD: ${DB_PASSWORD}
+      FMP_API_KEY: ${FMP_API_KEY}
+    restart: always
+    volumes:
+      - ./logs/api-server:/logs
+
+  batch:
+    container_name: batch
+    depends_on:
+      - db
+    image: ${NCP_CONTAINER_REGISTRY_BATCH}/payout-batch
+    environment:
+      DB_HOSTNAME: ${DB_HOSTNAME}
+      DB_PORT: ${DB_PORT}
+      DB_DATABASE: ${DB_DATABASE}
+      DB_USERNAME: ${DB_USERNAME}
+      DB_PASSWORD: ${DB_PASSWORD}
+      FMP_API_KEY: ${FMP_API_KEY}
+      NINJAS_API_KEY: ${NINJAS_API_KEY}
+    restart: always
+    volumes:
+      - ./logs/batch:/logs
+
+  db:
+    container_name: db
+    image: mysql:8.0
+    platform: linux/amd64
+    volumes:
+      - ./db/data:/var/lib/mysql
+    environment:
+      MYSQL_ROOT_HOST: '%'
+      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
+      MYSQL_DATABASE: ${DB_DATABASE}
+      MYSQL_USER: ${DB_USERNAME}
+      MYSQL_PASSWORD: ${DB_PASSWORD}
+    ports:
+      - ${DB_PORT}:${DB_PORT}
diff --git a/domain/build.gradle b/domain/build.gradle
index d1cae002..79aaaee1 100644
--- a/domain/build.gradle
+++ b/domain/build.gradle
@@ -1,7 +1,14 @@
+buildscript {
+    ext {
+        queryDslVersion = "5.0.0"
+    }
+}
+
 plugins {
     id 'java'
     id 'org.springframework.boot' version '3.2.1'
     id 'io.spring.dependency-management' version '1.1.4'
+    id 'java-test-fixtures'
 }
 
 repositories {
@@ -18,16 +25,55 @@ bootJar.enabled = false
 jar.enabled = true
 
 dependencies {
+    implementation(project(":core"))
+
+    // lombok
     compileOnly 'org.projectlombok:lombok'
     annotationProcessor 'org.projectlombok:lombok'
 
+    // Spring Data JPA
     implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+
+    // Spring Boot test
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
 
+    // Spring Web
+    implementation 'org.springframework.boot:spring-boot-starter-web'
+
+    // Mac M1 OS
+    implementation 'io.netty:netty-resolver-dns-native-macos:4.1.68.Final:osx-aarch_64'
+
+    // QueryDSL
+    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
+    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
+    annotationProcessor "jakarta.annotation:jakarta.annotation-api"
+    annotationProcessor "jakarta.persistence:jakarta.persistence-api"
+
+    // MySQL
     runtimeOnly 'com.mysql:mysql-connector-j'
+
+    // H2 Database
     runtimeOnly 'com.h2database:h2'
+
+    implementation 'org.flywaydb:flyway-core'
+    implementation 'org.flywaydb:flyway-mysql'
 }
 
 tasks.named('test') {
     useJUnitPlatform()
 }
+
+// QueryDSL
+def generated = 'src/main/generated'
+
+tasks.withType(JavaCompile) {
+    options.getGeneratedSourceOutputDirectory().set(file(generated))
+}
+
+sourceSets {
+    main.java.srcDirs += [ generated ]
+}
+
+clean {
+    delete file(generated)
+}
\ No newline at end of file
diff --git a/domain/src/main/generated/nexters/payout/domain/QBaseEntity.java b/domain/src/main/generated/nexters/payout/domain/QBaseEntity.java
new file mode 100644
index 00000000..420f56d4
--- /dev/null
+++ b/domain/src/main/generated/nexters/payout/domain/QBaseEntity.java
@@ -0,0 +1,39 @@
+package nexters.payout.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QBaseEntity is a Querydsl query type for BaseEntity
+ */
+@Generated("com.querydsl.codegen.DefaultSupertypeSerializer")
+public class QBaseEntity extends EntityPathBase<BaseEntity> {
+
+    private static final long serialVersionUID = -300935343L;
+
+    public static final QBaseEntity baseEntity = new QBaseEntity("baseEntity");
+
+    public final DateTimePath<java.time.Instant> createdAt = createDateTime("createdAt", java.time.Instant.class);
+
+    public final DateTimePath<java.time.Instant> lastModifiedAt = createDateTime("lastModifiedAt", java.time.Instant.class);
+
+    public QBaseEntity(String variable) {
+        super(BaseEntity.class, forVariable(variable));
+    }
+
+    public QBaseEntity(Path<? extends BaseEntity> path) {
+        super(path.getType(), path.getMetadata());
+    }
+
+    public QBaseEntity(PathMetadata metadata) {
+        super(BaseEntity.class, metadata);
+    }
+
+}
+
diff --git a/domain/src/main/generated/nexters/payout/domain/dividend/domain/QDividend.java b/domain/src/main/generated/nexters/payout/domain/dividend/domain/QDividend.java
new file mode 100644
index 00000000..2d5f4b21
--- /dev/null
+++ b/domain/src/main/generated/nexters/payout/domain/dividend/domain/QDividend.java
@@ -0,0 +1,55 @@
+package nexters.payout.domain.dividend.domain;
+
+import static com.querydsl.core.types.PathMetadataFactory.*;
+
+import com.querydsl.core.types.dsl.*;
+
+import com.querydsl.core.types.PathMetadata;
+import javax.annotation.processing.Generated;
+import com.querydsl.core.types.Path;
+
+
+/**
+ * QDividend is a Querydsl query type for Dividend
+ */
+@Generated("com.querydsl.codegen.DefaultEntitySerializer")
+public class QDividend extends EntityPathBase<Dividend> {
+
+    private static final long serialVersionUID = -1959252905L;
+
+    public static final QDividend dividend1 = new QDividend("dividend1");
+
+    public final nexters.payout.domain.QBaseEntity _super = new nexters.payout.domain.QBaseEntity(this);
+
+    //inherited
+    public final DateTimePath<java.time.Instant> createdAt = _super.createdAt;
+
+    public final DateTimePath<java.time.Instant> declarationDate = createDateTime("declarationDate", java.time.Instant.class);
+
+    public final NumberPath<Double> dividend = createNumber("dividend", Double.class);
+
+    public final DateTimePath<java.time.Instant> exDividendDate = createDateTime("exDividendDate", java.time.Instant.class);
+
+    public final ComparablePath<java.util.UUID> id = createComparable("id", java.util.UUID.class);
+
+    //inherited
+    public final DateTimePath<java.time.Instant> lastModifiedAt = _super.lastModifiedAt;
+
+    public final DateTimePath<java.time.Instant> paymentDate = createDateTime("paymentDate", java.time.Instant.class);
+
+    public final ComparablePath<java.util.UUID> stockId = createComparable("stockId", java.util.UUID.class);
+
+    public QDividend(String variable) {
+        super(Dividend.class, forVariable(variable));
+    }
+
+    public QDividend(Path<? extends Dividend> path) {
+        super(path.getType(), path.getMetadata());
+    }
+
+    public QDividend(PathMetadata metadata) {
+        super(Dividend.class, metadata);
+    }
+
+}
+
diff --git a/domain/src/main/java/nexters/dividend/domain/BaseEntity.java b/domain/src/main/java/nexters/dividend/domain/BaseEntity.java
deleted file mode 100644
index 69747dc6..00000000
--- a/domain/src/main/java/nexters/dividend/domain/BaseEntity.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package nexters.dividend.domain;
-
-import jakarta.persistence.*;
-import lombok.Getter;
-import org.springframework.data.annotation.CreatedDate;
-import org.springframework.data.annotation.LastModifiedDate;
-import org.springframework.data.jpa.domain.support.AuditingEntityListener;
-
-import java.time.Instant;
-import java.util.Objects;
-import java.util.UUID;
-
-/**
- * 생성일자, 마지막으로 수정된 일자 등 엔티티 별 공통으로 사용되는 클래스입니다.
- *
- * @author Min Ho CHO
- */
-@MappedSuperclass
-@EntityListeners(AuditingEntityListener.class)
-@Getter
-public class BaseEntity {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.UUID)
-    @Column(unique = true, nullable = false, updatable = false)
-    private UUID id;
-
-    @Column(name = "created_at", updatable = false)
-    @CreatedDate
-    private Instant createdAt;
-
-    @Column(name = "last_modified_at")
-    @LastModifiedDate
-    private Instant lastModifiedAt;
-
-    /**
-     * 엔티티 클래스의 hash code 함수를 재정의한 메서드입니다.
-     * @return object의 id 기반으로 생성된 hash code
-     */
-    @Override
-    public int hashCode() {
-        return Objects.hash(id);
-    }
-
-    /**
-     * 엔티티 클래스의 equals 함수를 재정의한 메서드입니다.
-     * @param obj 비교할 object
-     * @return 해당 object가 BaseEntity 타입이면서, 같은 id를 가지고 있는지 여부
-     */
-    @Override
-    public boolean equals(Object obj) {
-        return obj instanceof BaseEntity && this.id.equals(((BaseEntity) obj).getId());
-    }
-}
diff --git a/domain/src/main/java/nexters/dividend/domain/dividend/Dividend.java b/domain/src/main/java/nexters/dividend/domain/dividend/Dividend.java
deleted file mode 100644
index 6fd57d64..00000000
--- a/domain/src/main/java/nexters/dividend/domain/dividend/Dividend.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package nexters.dividend.domain.dividend;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import nexters.dividend.domain.BaseEntity;
-
-import java.time.Instant;
-import java.util.UUID;
-
-/**
- * 배당금을 표현하는 클래스입니다.
- *
- * @author Min Ho CHO
- */
-@Entity
-@Getter
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
-public class Dividend extends BaseEntity {
-
-    @Column(nullable = false, updatable = false)
-    private UUID stockId;
-
-    @Column(nullable = false)
-    private Integer dividend;
-
-    @Column(nullable = false, updatable = false)
-    private Instant exDividendDate;
-
-    @Column(nullable = false)
-    private Instant paymentDate;
-
-    @Column(nullable = false)
-    private Instant declarationDate;
-
-    private Dividend(
-            UUID stockId,
-            Integer dividend,
-            Instant exDividendDate,
-            Instant paymentDate,
-            Instant declarationDate) {
-        this.stockId = stockId;
-        this.dividend = dividend;
-        this.exDividendDate = exDividendDate;
-        this.paymentDate = paymentDate;
-        this.declarationDate = declarationDate;
-    }
-
-    public static Dividend createDividend(
-            UUID stockId,
-            Integer dividend,
-            Instant exDividendDate,
-            Instant paymentDate,
-            Instant declarationDate) {
-        return new Dividend(stockId, dividend, exDividendDate, paymentDate, declarationDate);
-    }
-}
diff --git a/domain/src/main/java/nexters/dividend/domain/dividend/repository/DividendRepository.java b/domain/src/main/java/nexters/dividend/domain/dividend/repository/DividendRepository.java
deleted file mode 100644
index 703d5821..00000000
--- a/domain/src/main/java/nexters/dividend/domain/dividend/repository/DividendRepository.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package nexters.dividend.domain.dividend.repository;
-
-import nexters.dividend.domain.dividend.Dividend;
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.UUID;
-
-/**
- * 배당금 JPA repository 클래스입니다.
- *
- * @author Min Ho CHO
- */
-public interface DividendRepository extends JpaRepository<Dividend, UUID> {
-
-}
diff --git a/domain/src/main/java/nexters/dividend/domain/stock/Sector.java b/domain/src/main/java/nexters/dividend/domain/stock/Sector.java
deleted file mode 100644
index a074b434..00000000
--- a/domain/src/main/java/nexters/dividend/domain/stock/Sector.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package nexters.dividend.domain.stock;
-
-public enum Sector {
-    TECHNOLOGY("Technology"),
-    COMMUNICATION_SERVICES("Communication Services"),
-    HEALTHCARE("Healthcare"),
-    CONSUMER_CYCLICAL("Consumer Cyclical"),
-    CONSUMER_DEFENSIVE("Consumer Defensive"),
-    BASIC_MATERIALS("Basic Materials"),
-    FINANCIAL_SERVICES("Financial Services"),
-    INDUSTRIALS("Industrials"),
-    REAL_ESTATE("Real Estate"),
-    ENERGY("Energy"),
-    UTILITIES("Utilities"),
-    ETC("ETC");
-
-    private final String value;
-
-    Sector(final String value) {
-        this.value = value;
-    }
-}
diff --git a/domain/src/main/java/nexters/dividend/domain/stock/Stock.java b/domain/src/main/java/nexters/dividend/domain/stock/Stock.java
deleted file mode 100644
index 8816e847..00000000
--- a/domain/src/main/java/nexters/dividend/domain/stock/Stock.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package nexters.dividend.domain.stock;
-
-import jakarta.persistence.*;
-import lombok.AccessLevel;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import nexters.dividend.domain.BaseEntity;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@Entity
-@Getter
-@NoArgsConstructor(access = AccessLevel.PROTECTED)
-class Stock extends BaseEntity {
-
-    @Column(unique = true, nullable = false, length = 10)
-    private String ticker;
-
-    @Column(nullable = false)
-    private String name;
-
-    @Enumerated(EnumType.STRING)
-    private Sector sector;
-
-    @ElementCollection
-    private List<String> dividendCycle = new ArrayList<>();
-
-    @Column(length = 10)
-    private String exchange;
-
-    private String industry;
-
-    private Double price;
-
-    private Integer volume;
-}
diff --git a/domain/src/main/java/nexters/dividend/domain/stock/StockRepository.java b/domain/src/main/java/nexters/dividend/domain/stock/StockRepository.java
deleted file mode 100644
index 0d854ff4..00000000
--- a/domain/src/main/java/nexters/dividend/domain/stock/StockRepository.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package nexters.dividend.domain.stock;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-
-import java.util.UUID;
-
-public interface StockRepository extends JpaRepository<Stock, UUID> {
-
-}
diff --git a/domain/src/main/java/nexters/payout/domain/BaseEntity.java b/domain/src/main/java/nexters/payout/domain/BaseEntity.java
new file mode 100644
index 00000000..22ca1dc5
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/BaseEntity.java
@@ -0,0 +1,26 @@
+package nexters.payout.domain;
+
+import jakarta.persistence.*;
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.UUID;
+
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+@Getter
+public class BaseEntity {
+
+    @Column(name = "created_at", updatable = false)
+    @CreatedDate
+    private Instant createdAt;
+
+    @Column(name = "last_modified_at")
+    @LastModifiedDate
+    private Instant lastModifiedAt;
+
+}
diff --git a/domain/src/main/java/nexters/dividend/domain/DomainApplication.java b/domain/src/main/java/nexters/payout/domain/DomainApplication.java
similarity index 89%
rename from domain/src/main/java/nexters/dividend/domain/DomainApplication.java
rename to domain/src/main/java/nexters/payout/domain/DomainApplication.java
index c3545a19..46a76ccd 100644
--- a/domain/src/main/java/nexters/dividend/domain/DomainApplication.java
+++ b/domain/src/main/java/nexters/payout/domain/DomainApplication.java
@@ -1,4 +1,4 @@
-package nexters.dividend.domain;
+package nexters.payout.domain;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
diff --git a/domain/src/main/java/nexters/payout/domain/common/config/DomainService.java b/domain/src/main/java/nexters/payout/domain/common/config/DomainService.java
new file mode 100644
index 00000000..ab9d6141
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/common/config/DomainService.java
@@ -0,0 +1,12 @@
+package nexters.payout.domain.common.config;
+
+import org.springframework.stereotype.Component;
+
+import java.lang.annotation.*;
+
+@Component
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface DomainService {
+}
\ No newline at end of file
diff --git a/domain/src/main/java/nexters/payout/domain/common/config/JpaConfig.java b/domain/src/main/java/nexters/payout/domain/common/config/JpaConfig.java
new file mode 100644
index 00000000..e136d6a8
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/common/config/JpaConfig.java
@@ -0,0 +1,9 @@
+package nexters.payout.domain.common.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaConfig {
+}
diff --git a/domain/src/main/java/nexters/payout/domain/common/config/QueryDslConfig.java b/domain/src/main/java/nexters/payout/domain/common/config/QueryDslConfig.java
new file mode 100644
index 00000000..3c8564c9
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/common/config/QueryDslConfig.java
@@ -0,0 +1,19 @@
+package nexters.payout.domain.common.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class QueryDslConfig {
+
+    @PersistenceContext
+    private EntityManager em;
+
+    @Bean
+    public JPAQueryFactory jpaQueryFactory() {
+        return new JPAQueryFactory(em);
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/dividend/application/DividendCommandService.java b/domain/src/main/java/nexters/payout/domain/dividend/application/DividendCommandService.java
new file mode 100644
index 00000000..48b588e9
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/dividend/application/DividendCommandService.java
@@ -0,0 +1,34 @@
+package nexters.payout.domain.dividend.application;
+
+import lombok.RequiredArgsConstructor;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.domain.repository.DividendRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class DividendCommandService {
+
+    private final DividendRepository dividendRepository;
+
+    public void saveOrUpdate(UUID stockId, Dividend dividendData) {
+        dividendRepository.findByStockIdAndExDividendDate(stockId, dividendData.getExDividendDate())
+                .ifPresentOrElse(
+                        existing -> existing.update(
+                                dividendData.getDividend(),
+                                dividendData.getPaymentDate(),
+                                dividendData.getDeclarationDate()
+                        ),
+                        () -> dividendRepository.save(dividendData)
+                );
+    }
+
+    public void deleteInvalidDividend() {
+        dividendRepository.deleteByYearAndCreatedAt(InstantProvider.getThisYear(), InstantProvider.getYesterday());
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/dividend/application/dto/UpdateDividendRequest.java b/domain/src/main/java/nexters/payout/domain/dividend/application/dto/UpdateDividendRequest.java
new file mode 100644
index 00000000..36235eb1
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/dividend/application/dto/UpdateDividendRequest.java
@@ -0,0 +1,10 @@
+package nexters.payout.domain.dividend.application.dto;
+
+import java.time.Instant;
+
+public record UpdateDividendRequest(
+        Double dividend,
+        Instant paymentDate,
+        Instant declarationDate
+) {
+}
diff --git a/domain/src/main/java/nexters/payout/domain/dividend/domain/Dividend.java b/domain/src/main/java/nexters/payout/domain/dividend/domain/Dividend.java
new file mode 100644
index 00000000..15561333
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/dividend/domain/Dividend.java
@@ -0,0 +1,81 @@
+package nexters.payout.domain.dividend.domain;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import nexters.payout.domain.BaseEntity;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.UUID;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Dividend extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID id;
+
+    @Column(nullable = false, updatable = false)
+    private UUID stockId;
+
+    private Double dividend;
+
+    @Column(updatable = false)
+    private Instant exDividendDate;
+
+    private Instant paymentDate;
+
+    private Instant declarationDate;
+
+    public Dividend(final UUID id, final UUID stockId, final Double dividend, final Instant exDividendDate,
+                    final Instant paymentDate, final Instant declarationDate) {
+        this.id = id;
+        this.stockId = stockId;
+        this.dividend = dividend;
+        this.exDividendDate = exDividendDate;
+        this.paymentDate = paymentDate;
+        this.declarationDate = declarationDate;
+    }
+
+    public Dividend(final UUID stockId, final Double dividend, final Instant exDividendDate,
+                    final Instant paymentDate, final Instant declarationDate) {
+        this(null, stockId, dividend, exDividendDate, paymentDate, declarationDate);
+    }
+
+    public void update(final Double dividend, final Instant paymentDate, final Instant declarationDate) {
+        this.dividend = dividend;
+        this.paymentDate = paymentDate;
+        this.declarationDate = declarationDate;
+    }
+
+    public static Dividend create(
+            final UUID stockId, final Double dividend, final Instant exDividendDate,
+            final Instant paymentDate, final Instant declarationDate) {
+        return new Dividend(stockId, dividend, exDividendDate, paymentDate, declarationDate);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof Dividend && this.id.equals(((Dividend) obj).getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public String toString() {
+        return "Dividend{" +
+                "stockId=" + stockId +
+                ", dividend=" + dividend +
+                ", exDividendDate=" + exDividendDate +
+                ", paymentDate=" + paymentDate +
+                ", declarationDate=" + declarationDate +
+                '}';
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/dividend/domain/repository/DividendRepository.java b/domain/src/main/java/nexters/payout/domain/dividend/domain/repository/DividendRepository.java
new file mode 100644
index 00000000..3412fb1a
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/dividend/domain/repository/DividendRepository.java
@@ -0,0 +1,15 @@
+package nexters.payout.domain.dividend.domain.repository;
+
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.dividend.infra.DividendRepositoryCustom;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.UUID;
+
+public interface DividendRepository extends JpaRepository<Dividend, UUID>, DividendRepositoryCustom {
+
+    List<Dividend> findAllByStockId(UUID stockId);
+
+    List<Dividend> findAllByStockIdIn(List<UUID> stockIds);
+}
diff --git a/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java
new file mode 100644
index 00000000..dbd020d6
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryCustom.java
@@ -0,0 +1,17 @@
+package nexters.payout.domain.dividend.infra;
+
+
+import nexters.payout.domain.dividend.domain.Dividend;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface DividendRepositoryCustom {
+
+    Optional<Dividend> findByStockIdAndExDividendDate(UUID stockId, Instant date);
+    List<Dividend> findAllByTickerAndYearAndMonth(String ticker, Integer year, Integer month);
+    List<Dividend> findAllByTickerAndYear(String ticker, Integer year);
+    void deleteByYearAndCreatedAt(Integer year, Instant createdAt);
+}
diff --git a/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java
new file mode 100644
index 00000000..cbf7b9e7
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/dividend/infra/DividendRepositoryImpl.java
@@ -0,0 +1,76 @@
+package nexters.payout.domain.dividend.infra;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import jakarta.persistence.EntityManager;
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.dividend.domain.Dividend;
+import org.springframework.stereotype.Repository;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static nexters.payout.domain.dividend.domain.QDividend.dividend1;
+import static nexters.payout.domain.stock.domain.QStock.stock;
+
+@Repository
+public class DividendRepositoryImpl implements DividendRepositoryCustom {
+
+    private final JPAQueryFactory queryFactory;
+
+    public DividendRepositoryImpl(EntityManager em) {
+        this.queryFactory = new JPAQueryFactory(em);
+    }
+
+    @Override
+    public Optional<Dividend> findByStockIdAndExDividendDate(UUID stockId, Instant date) {
+
+        return Optional.ofNullable(
+                queryFactory
+                        .selectFrom(dividend1)
+                        .innerJoin(stock).on(dividend1.stockId.eq(stock.id))
+                        .where(stock.id.eq(stockId)
+                                .and(dividend1.exDividendDate.year().eq(InstantProvider.getYear(date)))
+                                .and(dividend1.exDividendDate.month().eq(InstantProvider.getMonth(date)))
+                                .and(dividend1.exDividendDate.dayOfMonth().eq(InstantProvider.getDayOfMonth(date))))
+                        .fetchOne()
+        );
+    }
+
+    @Override
+    public List<Dividend> findAllByTickerAndYearAndMonth(String ticker, Integer year, Integer month) {
+
+        return queryFactory
+                .selectFrom(dividend1)
+                .innerJoin(stock).on(dividend1.stockId.eq(stock.id))
+                .where(dividend1.exDividendDate.year().eq(year)
+                        .and(dividend1.exDividendDate.month().eq(month))
+                        .and(stock.ticker.eq(ticker)))
+                .fetch();
+    }
+
+    @Override
+    public List<Dividend> findAllByTickerAndYear(String ticker, Integer year) {
+
+        return queryFactory
+                .selectFrom(dividend1)
+                .innerJoin(stock).on(dividend1.stockId.eq(stock.id))
+                .where(dividend1.exDividendDate.year().eq(year)
+                        .and(stock.ticker.eq(ticker)))
+                .fetch();
+    }
+
+    @Override
+    public void deleteByYearAndCreatedAt(Integer year, Instant createdAt) {
+
+        queryFactory
+                .delete(dividend1)
+                .where(dividend1.exDividendDate.year().eq(year)
+                        .and(dividend1.createdAt.year().eq(InstantProvider.getYear(createdAt)))
+                        .and(dividend1.createdAt.month().eq(InstantProvider.getMonth(createdAt)))
+                        .and(dividend1.createdAt.dayOfMonth().eq(InstantProvider.getDayOfMonth(createdAt))))
+                .execute();
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/application/StockCommandService.java b/domain/src/main/java/nexters/payout/domain/stock/application/StockCommandService.java
new file mode 100644
index 00000000..ea8b71cd
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/application/StockCommandService.java
@@ -0,0 +1,34 @@
+package nexters.payout.domain.stock.application;
+
+import lombok.RequiredArgsConstructor;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.repository.StockRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class StockCommandService {
+
+    private final StockRepository stockRepository;
+
+    public void create(Stock stockData) {
+        stockRepository.save(stockData);
+    }
+
+    public void update(String ticker, Stock stockData) {
+        stockRepository.findByTicker(ticker)
+                .ifPresent(
+                        existing -> existing.update(stockData.getPrice(), stockData.getVolume(), stockData.getSector())
+                );
+    }
+
+    public void saveOrUpdate(String ticker, Stock stockData) {
+        stockRepository.findByTicker(ticker)
+                .ifPresentOrElse(
+                        existing -> existing.update(stockData.getPrice(), stockData.getVolume(), stockData.getSector()),
+                        () -> stockRepository.save(stockData)
+                );
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/application/dto/UpdateStockRequest.java b/domain/src/main/java/nexters/payout/domain/stock/application/dto/UpdateStockRequest.java
new file mode 100644
index 00000000..4868f9c7
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/application/dto/UpdateStockRequest.java
@@ -0,0 +1,7 @@
+package nexters.payout.domain.stock.application.dto;
+
+public record UpdateStockRequest(
+        Double price,
+        Integer volume
+) {
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/Exchange.java b/domain/src/main/java/nexters/payout/domain/stock/domain/Exchange.java
new file mode 100644
index 00000000..b373d7d7
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/Exchange.java
@@ -0,0 +1,17 @@
+package nexters.payout.domain.stock.domain;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public enum Exchange {
+    NASDAQ,
+    NYSE,
+    AMEX;
+
+    public static List<String> getNames() {
+        return Arrays.stream(Exchange.values())
+                .map(Enum::name)
+                .collect(Collectors.toList());
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/Sector.java b/domain/src/main/java/nexters/payout/domain/stock/domain/Sector.java
new file mode 100644
index 00000000..2fc5cc76
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/Sector.java
@@ -0,0 +1,80 @@
+package nexters.payout.domain.stock.domain;
+
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Getter
+public enum Sector {
+    TECHNOLOGY("Technology"),
+    COMMUNICATION_SERVICES("Communication Services"),
+    HEALTHCARE("Healthcare"),
+    CONSUMER_CYCLICAL("Consumer Cyclical"),
+    CONSUMER_DEFENSIVE("Consumer Defensive"),
+    BASIC_MATERIALS("Basic Materials"),
+    FINANCIAL_SERVICES("Financial Services"),
+    INDUSTRIALS("Industrials"),
+    REAL_ESTATE("Real Estate"),
+    ENERGY("Energy"),
+    UTILITIES("Utilities"),
+    INDUSTRIAL_GOODS("Industrial Goods"),
+    FINANCIAL("Financial"),
+    SERVICES("Services"),
+    CONGLOMERATES("Conglomerates"),
+    ETF("ETF"),
+    ETC("ETC");
+
+    private final String name;
+
+    Sector(final String name) {
+        this.name = name;
+    }
+
+    private static final Map<String, Sector> NAME_TO_SECTOR_MAP = Arrays
+            .stream(values())
+            .collect(Collectors.toMap(sector -> sector.name, Function.identity()));
+
+    private static final Set<String> ETC_NAMES = Set.of(
+            INDUSTRIAL_GOODS.name, FINANCIAL.name, SERVICES.name, CONGLOMERATES.name, ETC.name()
+    );
+
+    private static final Set<String> ETC_VALUES = Set.of(
+            INDUSTRIAL_GOODS.name(), FINANCIAL.name(), SERVICES.name(), CONGLOMERATES.name(), ETC.name()
+    );
+
+    public static List<String> getNames() {
+        return Arrays.stream(Sector.values())
+                .map(it -> it.name)
+                .filter(name -> !name.isEmpty())
+                .toList();
+    }
+
+    public static Sector fromName(String sectorName) {
+        if (sectorName == null || isEtcCategoryName(sectorName)) {
+            return ETC;
+        }
+
+        return NAME_TO_SECTOR_MAP.getOrDefault(sectorName, ETC);
+    }
+
+    public static Sector fromValue(String sectorValue) {
+        if (sectorValue == null || isEtcCategoryValue(sectorValue)) {
+            return ETC;
+        }
+
+        return Sector.valueOf(sectorValue);
+    }
+
+    private static boolean isEtcCategoryName(String value) {
+        return ETC_NAMES.contains(value);
+    }
+
+    private static boolean isEtcCategoryValue(String value) {
+        return ETC_VALUES.contains(value);
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/Stock.java b/domain/src/main/java/nexters/payout/domain/stock/domain/Stock.java
new file mode 100644
index 00000000..634de7df
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/Stock.java
@@ -0,0 +1,98 @@
+package nexters.payout.domain.stock.domain;
+
+import jakarta.persistence.*;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import nexters.payout.domain.BaseEntity;
+
+import java.util.Objects;
+import java.util.UUID;
+
+@Entity
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class Stock extends BaseEntity {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID id;
+
+    @Column(unique = true, nullable = false, length = 50)
+    private String ticker;
+
+    private String name;
+
+    @Enumerated(EnumType.STRING)
+    private Sector sector;
+
+    @Column(length = 10)
+    private String exchange;
+
+    private String industry;
+
+    private Double price;
+
+    private Integer volume;
+
+    private String logoUrl;
+
+    public Stock(final UUID id, final String ticker, final String name,
+                 final Sector sector, final String exchange, final String industry,
+                 final Double price, final Integer volume, final String logoUrl) {
+        validateTicker(ticker);
+        this.id = id;
+        this.ticker = ticker;
+        this.name = name;
+        this.sector = sector;
+        this.exchange = exchange;
+        this.industry = industry;
+        this.price = price;
+        this.volume = volume;
+        this.logoUrl = logoUrl;
+    }
+
+    public Stock(final String ticker, final String name,
+                 final Sector sector, final String exchange, final String industry,
+                 final Double price, final Integer volume, final String logoUrl) {
+        this(null, ticker, name, sector, exchange, industry, price, volume, logoUrl);
+    }
+
+    private void validateTicker(final String ticker) {
+        if (ticker.isBlank()) {
+            throw new IllegalArgumentException("ticker must not be null or empty");
+        }
+    }
+
+    public void update(
+            final Double price,
+            final Integer volume,
+            final Sector sector) {
+        this.price = price;
+        this.volume = volume;
+        this.sector = sector;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof Stock && this.id.equals(((Stock) obj).getId());
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id);
+    }
+
+    @Override
+    public String toString() {
+        return "Stock{" +
+                "ticker='" + ticker + '\'' +
+                ", name='" + name + '\'' +
+                ", sector=" + sector +
+                ", exchange='" + exchange + '\'' +
+                ", industry='" + industry + '\'' +
+                ", price=" + price +
+                ", volume=" + volume +
+                '}';
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/repository/StockRepository.java b/domain/src/main/java/nexters/payout/domain/stock/domain/repository/StockRepository.java
new file mode 100644
index 00000000..a0671465
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/repository/StockRepository.java
@@ -0,0 +1,15 @@
+package nexters.payout.domain.stock.domain.repository;
+
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.infra.StockRepositoryCustom;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface StockRepository extends JpaRepository<Stock, UUID>, StockRepositoryCustom {
+    Optional<Stock> findByTicker(String ticker);
+
+    List<Stock> findAllByTickerIn(List<String> tickers);
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/repository/dto/StockDividendDto.java b/domain/src/main/java/nexters/payout/domain/stock/domain/repository/dto/StockDividendDto.java
new file mode 100644
index 00000000..a4afb1f9
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/repository/dto/StockDividendDto.java
@@ -0,0 +1,10 @@
+package nexters.payout.domain.stock.domain.repository.dto;
+
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+
+public record StockDividendDto(
+        Stock stock,
+        Dividend dividend
+) {
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/service/SectorAnalysisService.java b/domain/src/main/java/nexters/payout/domain/stock/domain/service/SectorAnalysisService.java
new file mode 100644
index 00000000..2470f356
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/service/SectorAnalysisService.java
@@ -0,0 +1,85 @@
+package nexters.payout.domain.stock.domain.service;
+
+import nexters.payout.domain.common.config.DomainService;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@DomainService
+public class SectorAnalysisService {
+
+    public Map<Sector, SectorInfo> calculateSectorRatios(final List<StockShare> stockShares) {
+        Map<Sector, Integer> sectorCountMap = getSectorCountMap(stockShares);
+        Map<Sector, List<StockShare>> sectorStockMap = getSectorStockMap(stockShares);
+        double totalValue = totalValue(stockShares);
+
+        Map<Sector, SectorInfo> sectorInfoMap = new HashMap<>();
+
+        for (Sector sector : Sector.values()) {
+            if (stockCountBySector(sectorCountMap, sector) > 0) {
+                Double sectorRatio = totalValueBySector(stockShares, sector) / totalValue;
+                sectorInfoMap.put(sector, new SectorInfo(sectorRatio, getStocks(sectorStockMap, sector)));
+            }
+        }
+
+        return sectorInfoMap;
+    }
+
+    private Map<Sector, Integer> getSectorCountMap(final List<StockShare> stockShares) {
+        return stockShares
+                .stream()
+                .map(stockShare -> stockShare.stock().getSector())
+                .collect(Collectors.groupingBy(Function.identity(),
+                        Collectors.collectingAndThen(Collectors.counting(), Long::intValue)));
+    }
+
+    private Map<Sector, List<StockShare>> getSectorStockMap(final List<StockShare> stockShares) {
+        return stockShares
+                .stream()
+                .collect(Collectors.groupingBy(stockShare -> stockShare.stock().getSector()));
+    }
+
+    private static double totalValue(final List<StockShare> stockShares) {
+        return stockShares
+                .stream()
+                .mapToDouble(stockShare -> stockShare.share() * stockShare.stock().getPrice())
+                .sum();
+    }
+
+    private List<StockShare> getStocks(final Map<Sector, List<StockShare>> sectorStockMap, final Sector sector) {
+        return sectorStockMap.getOrDefault(sector, Collections.emptyList());
+    }
+
+    private Integer stockCountBySector(final Map<Sector, Integer> sectorCountMap, final Sector sector) {
+        return sectorCountMap.getOrDefault(sector, 0);
+    }
+
+    private double totalValueBySector(final List<StockShare> stockShares, final Sector sector) {
+        return stockShares
+                .stream()
+                .filter(share -> share.stock().getSector().equals(sector))
+                .mapToDouble(stockShare -> stockShare.share() * stockShare.stock().getPrice())
+                .sum();
+    }
+
+    public record SectorInfo(
+            Double ratio,
+            List<StockShare> stockShares
+    ) {
+
+    }
+
+    public record StockShare(
+            Stock stock,
+            Integer share
+    ) {
+
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/domain/service/StockDividendAnalysisService.java b/domain/src/main/java/nexters/payout/domain/stock/domain/service/StockDividendAnalysisService.java
new file mode 100644
index 00000000..611dabdf
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/domain/service/StockDividendAnalysisService.java
@@ -0,0 +1,89 @@
+package nexters.payout.domain.stock.domain.service;
+
+import nexters.payout.core.time.InstantProvider;
+import nexters.payout.domain.common.config.DomainService;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.time.LocalDate;
+import java.time.Month;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@DomainService
+public class StockDividendAnalysisService {
+    /**
+     * 작년 데이터를 기반으로 배당을 주었던 월 리스트를 계산합니다.
+     */
+    public List<Month> calculateDividendMonths(final Stock stock, final List<Dividend> dividends) {
+        int lastYear = InstantProvider.getLastYear();
+
+        return dividends
+                .stream()
+                .filter(dividend -> stock.getId().equals(dividend.getStockId()))
+                .map(dividend -> InstantProvider.toLocalDate(dividend.getExDividendDate()))
+                .filter(exDividendDate -> exDividendDate.getYear() == lastYear)
+                .map(LocalDate::getMonth)
+                .distinct()
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 배당 수익률은 (연간 배당금 / 현재가) 를 기준으로 합니다.
+     */
+    public Double calculateDividendYield(final Stock stock, final List<Dividend> dividends) {
+        double sumOfDividend = dividends
+                .stream()
+                .mapToDouble(Dividend::getDividend)
+                .sum();
+
+        Double stockPrice = stock.getPrice();
+
+        if (stockPrice == null || stockPrice == 0) {
+            return 0.0;
+        }
+
+        return sumOfDividend / stockPrice;
+    }
+
+    /**
+     * 공시된 현재 연도의 데이터가 있는 경우 실제 지급일을 반환하고, 없으면 작년 데이터를 기반으로 가장 빠른 배당 지급일을 계산합니다.
+     * 월과 일만 확인하기 때문에 과거 연도가 반환될 수 있습니다.
+     */
+    public Optional<Dividend> findUpcomingDividend(
+            final List<Dividend> lastYearDividends, final List<Dividend> thisYearDividends
+    ) {
+        LocalDate now = InstantProvider.getNow();
+
+        for (Dividend dividend : thisYearDividends) {
+            LocalDate exDividendDate = InstantProvider.toLocalDate(dividend.getExDividendDate());
+            if (exDividendDate.getYear() == now.getYear() && (isCurrentOrFutureDate(exDividendDate))) {
+                return Optional.of(dividend);
+            }
+        }
+
+        return lastYearDividends
+                .stream()
+                .map(dividend -> {
+                    LocalDate exDividendDate = InstantProvider.toLocalDate(dividend.getExDividendDate());
+                    LocalDate adjustedExDividendDate = exDividendDate.withYear(now.getYear());
+                    return new AbstractMap.SimpleEntry<>(dividend, adjustedExDividendDate);
+                })
+                .filter(date -> isCurrentOrFutureDate(date.getValue()))
+                .min(Map.Entry.comparingByValue())
+                .map(Map.Entry::getKey);
+    }
+
+    private boolean isCurrentOrFutureDate(final LocalDate date) {
+        LocalDate now = InstantProvider.getNow();
+        return date.isEqual(now) || date.isAfter(InstantProvider.getNow());
+    }
+
+    public Double calculateAverageDividend(final List<Dividend> dividends) {
+        return dividends
+                .stream()
+                .mapToDouble(Dividend::getDividend)
+                .average()
+                .orElse(0.0);
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/infra/StockRepositoryCustom.java b/domain/src/main/java/nexters/payout/domain/stock/infra/StockRepositoryCustom.java
new file mode 100644
index 00000000..ee12f309
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/infra/StockRepositoryCustom.java
@@ -0,0 +1,15 @@
+package nexters.payout.domain.stock.infra;
+
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.infra.dto.StockDividendDto;
+import nexters.payout.domain.stock.infra.dto.StockDividendYieldDto;
+
+import java.util.List;
+
+public interface StockRepositoryCustom {
+
+    List<Stock> findStocksByTickerOrNameWithPriority(String search, Integer pageNumber, Integer pageSize);
+    List<StockDividendDto> findUpcomingDividendStock(Sector sector, int pageNumber, int pageSize);
+    List<StockDividendYieldDto> findBiggestDividendYieldStock(int lastYear, Sector sector, int pageNumber, int pageSize);
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/infra/StockRepositoryImpl.java b/domain/src/main/java/nexters/payout/domain/stock/infra/StockRepositoryImpl.java
new file mode 100644
index 00000000..1d02e905
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/infra/StockRepositoryImpl.java
@@ -0,0 +1,90 @@
+package nexters.payout.domain.stock.infra;
+
+import com.querydsl.core.types.OrderSpecifier;
+import com.querydsl.core.types.Projections;
+import com.querydsl.core.types.dsl.BooleanExpression;
+import com.querydsl.core.types.dsl.CaseBuilder;
+import com.querydsl.core.types.dsl.NumberExpression;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import nexters.payout.domain.stock.domain.QStock;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.infra.dto.StockDividendDto;
+import nexters.payout.domain.stock.infra.dto.StockDividendYieldDto;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static java.time.ZoneOffset.UTC;
+import static nexters.payout.domain.dividend.domain.QDividend.dividend1;
+import static nexters.payout.domain.stock.domain.QStock.stock;
+
+@Repository
+@RequiredArgsConstructor
+public class StockRepositoryImpl implements StockRepositoryCustom {
+
+    private final Double MAX_DIVIDEND_YIELD = 0.9;
+    private final JPAQueryFactory queryFactory;
+
+    @Override
+    public List<Stock> findStocksByTickerOrNameWithPriority(String keyword, Integer pageNumber, Integer pageSize) {
+        QStock stock = QStock.stock;
+
+        // 검색 조건
+        BooleanExpression tickerStartsWith = stock.ticker.startsWith(keyword);
+        BooleanExpression nameContains = stock.name.contains(keyword);
+
+        // 정렬 조건
+        OrderSpecifier<Integer> orderByPriority = new CaseBuilder()
+                .when(tickerStartsWith).then(1)
+                .when(nameContains).then(2)
+                .otherwise(3)
+                .asc();
+        OrderSpecifier<String> orderByTicker = stock.ticker.asc();
+        OrderSpecifier<String> orderByName = stock.name.asc();
+
+        long offset = (long) (pageNumber - 1) * pageSize;
+
+        return queryFactory.selectFrom(stock)
+                .where(tickerStartsWith.or(nameContains))
+                .orderBy(orderByPriority, orderByTicker, orderByName)
+                .offset(offset)
+                .limit(pageSize)
+                .fetch();
+    }
+
+    @Override
+    public List<StockDividendDto> findUpcomingDividendStock(Sector sector, int pageNumber, int pageSize) {
+
+        return queryFactory
+                .select(Projections.constructor(StockDividendDto.class, stock, dividend1))
+                .from(stock)
+                .innerJoin(dividend1).on(stock.id.eq(dividend1.stockId))
+                .where(dividend1.exDividendDate.after(LocalDateTime.now().toInstant(UTC)).and(stock.sector.eq(sector)))
+                .orderBy(dividend1.exDividendDate.asc())
+                .offset((long) (pageNumber - 1) * pageSize)
+                .limit(pageSize)
+                .fetch();
+    }
+
+    @Override
+    public List<StockDividendYieldDto> findBiggestDividendYieldStock(int lastYear, Sector sector, int pageNumber, int pageSize) {
+
+        NumberExpression<Double> dividendYield = dividend1.dividend.sum().coalesce(1.0).divide(stock.price);
+
+        return queryFactory
+                .select(Projections.constructor(StockDividendYieldDto.class, stock, dividendYield))
+                .from(stock)
+                .innerJoin(dividend1)
+                .on(stock.id.eq(dividend1.stockId))
+                .where(dividend1.exDividendDate.year().eq(lastYear).and(stock.sector.eq(sector)))
+                .groupBy(stock.id, stock.price)
+                .orderBy(dividendYield.desc())
+                .having(dividendYield.lt(MAX_DIVIDEND_YIELD))
+                .offset((long) (pageNumber - 1) * pageSize)
+                .limit(pageSize)
+                .fetch();
+    }
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/infra/dto/StockDividendDto.java b/domain/src/main/java/nexters/payout/domain/stock/infra/dto/StockDividendDto.java
new file mode 100644
index 00000000..2dacfc28
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/infra/dto/StockDividendDto.java
@@ -0,0 +1,10 @@
+package nexters.payout.domain.stock.infra.dto;
+
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Stock;
+
+public record StockDividendDto(
+        Stock stock,
+        Dividend dividend
+) {
+}
diff --git a/domain/src/main/java/nexters/payout/domain/stock/infra/dto/StockDividendYieldDto.java b/domain/src/main/java/nexters/payout/domain/stock/infra/dto/StockDividendYieldDto.java
new file mode 100644
index 00000000..a04a3cda
--- /dev/null
+++ b/domain/src/main/java/nexters/payout/domain/stock/infra/dto/StockDividendYieldDto.java
@@ -0,0 +1,9 @@
+package nexters.payout.domain.stock.infra.dto;
+
+import nexters.payout.domain.stock.domain.Stock;
+
+public record StockDividendYieldDto(
+        Stock stock,
+        Double dividendYield
+) {
+}
diff --git a/domain/src/main/resources/application-dev.yml b/domain/src/main/resources/application-dev.yml
index 04b7b25c..a2d2e0ce 100644
--- a/domain/src/main/resources/application-dev.yml
+++ b/domain/src/main/resources/application-dev.yml
@@ -7,8 +7,16 @@ spring:
   jpa:
     database-platform: org.hibernate.dialect.MySQLDialect
     hibernate:
-      ddl-auto: create-drop
+      ddl-auto: validate
     properties:
       hibernate:
         format_sql: true
     show-sql: true
+
+  flyway:
+    enabled: true
+    baseline-on-migrate: true
+    url: jdbc:mysql://localhost:3306/nexters
+    user: test
+    password: test
+    baseline-version: 0
diff --git a/domain/src/main/resources/application-prod.yml b/domain/src/main/resources/application-prod.yml
index e69de29b..dfe83613 100644
--- a/domain/src/main/resources/application-prod.yml
+++ b/domain/src/main/resources/application-prod.yml
@@ -0,0 +1,23 @@
+spring:
+  datasource:
+    url: jdbc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE}
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+
+  jpa:
+    database-platform: org.hibernate.dialect.MySQLDialect
+    hibernate:
+      ddl-auto: update
+    properties:
+      hibernate:
+        format_sql: true
+    show-sql: true
+
+  flyway:
+    enabled: true
+    baseline-on-migrate: true
+    url: jdbc:mysql://${DB_HOSTNAME}:${DB_PORT}/${DB_DATABASE}
+    user: ${DB_USERNAME}
+    password: ${DB_PASSWORD}
+    baseline-version: 4
\ No newline at end of file
diff --git a/domain/src/main/resources/application-test.yml b/domain/src/main/resources/application-test.yml
index 7e5c6a60..8d239548 100644
--- a/domain/src/main/resources/application-test.yml
+++ b/domain/src/main/resources/application-test.yml
@@ -14,3 +14,6 @@ spring:
         format_sql: true
     show-sql: true
 
+  flyway:
+    enabled: false
+
diff --git a/domain/src/main/resources/application.yml b/domain/src/main/resources/application.yml
index 027b4e36..03c30d37 100644
--- a/domain/src/main/resources/application.yml
+++ b/domain/src/main/resources/application.yml
@@ -1,3 +1,3 @@
 spring:
   profiles:
-    active: test
\ No newline at end of file
+    active: test
diff --git a/domain/src/main/resources/db/migration/V1__init.sql b/domain/src/main/resources/db/migration/V1__init.sql
new file mode 100644
index 00000000..9531faf9
--- /dev/null
+++ b/domain/src/main/resources/db/migration/V1__init.sql
@@ -0,0 +1,30 @@
+create table stock
+(
+    id binary (16) not null
+        primary key,
+    price            double,
+    volume           int,
+    created_at       datetime(6),
+    last_modified_at datetime(6),
+    exchange         varchar(100),
+    ticker           varchar(100) not null,
+    industry         varchar(255),
+    name             varchar(255),
+    sector           enum('TECHNOLOGY', 'COMMUNICATION_SERVICES', 'HEALTHCARE', 'CONSUMER_CYCLICAL', 'CONSUMER_DEFENSIVE', 'BASIC_MATERIALS', 'FINANCIAL_SERVICES', 'INDUSTRIALS', 'REAL_ESTATE', 'ENERGY', 'UTILITIES', 'INDUSTRIAL_GOODS', 'FINANCIAL', 'SERVICES', 'CONGLOMERATES', 'ETC')
+) engine = innodb
+  default charset = utf8mb4;
+
+create table if not exists dividend
+(
+    id binary (16) not null,
+    created_at       datetime(6),
+    last_modified_at datetime(6),
+    declaration_date datetime(6) not null,
+    dividend         integer     not null,
+    ex_dividend_date datetime(6) not null,
+    payment_date     datetime(6) not null,
+    stock_id binary (16) not null,
+    FOREIGN KEY (stock_id) REFERENCES stock (id),
+    primary key (id)
+) engine = innodb
+  default charset = utf8mb4;
\ No newline at end of file
diff --git a/domain/src/main/resources/db/migration/V2__column_update.sql b/domain/src/main/resources/db/migration/V2__column_update.sql
new file mode 100644
index 00000000..d65d2a64
--- /dev/null
+++ b/domain/src/main/resources/db/migration/V2__column_update.sql
@@ -0,0 +1,24 @@
+alter table stock
+    modify exchange varchar (100) null;
+
+alter table stock
+    modify ticker varchar (100) null;
+
+alter table stock
+    modify name varchar (255) null;
+
+alter table dividend
+    modify dividend double null;
+
+alter table dividend
+    modify declaration_date datetime(6) null;
+
+alter table dividend
+    modify payment_date datetime(6) null;
+
+alter table dividend
+    modify ex_dividend_date datetime(6) null;
+
+
+
+
diff --git a/domain/src/main/resources/db/migration/V3__add_stock_logo_column.sql b/domain/src/main/resources/db/migration/V3__add_stock_logo_column.sql
new file mode 100644
index 00000000..413a267c
--- /dev/null
+++ b/domain/src/main/resources/db/migration/V3__add_stock_logo_column.sql
@@ -0,0 +1,2 @@
+alter table stock
+    add logo_url text null;
\ No newline at end of file
diff --git a/domain/src/main/resources/db/migration/V4__add_sector.sql b/domain/src/main/resources/db/migration/V4__add_sector.sql
new file mode 100644
index 00000000..5e345d65
--- /dev/null
+++ b/domain/src/main/resources/db/migration/V4__add_sector.sql
@@ -0,0 +1,2 @@
+alter table stock
+    modify sector enum ('TECHNOLOGY', 'COMMUNICATION_SERVICES', 'HEALTHCARE', 'CONSUMER_CYCLICAL', 'CONSUMER_DEFENSIVE', 'BASIC_MATERIALS', 'FINANCIAL_SERVICES', 'INDUSTRIALS', 'REAL_ESTATE', 'ENERGY', 'UTILITIES', 'INDUSTRIAL_GOODS', 'FINANCIAL', 'SERVICES', 'CONGLOMERATES', 'ETC', 'ETF') null;
diff --git a/domain/src/test/java/nexters/dividend/domain/DomainApplicationTests.java b/domain/src/test/java/nexters/payout/domain/DomainApplicationTests.java
similarity index 85%
rename from domain/src/test/java/nexters/dividend/domain/DomainApplicationTests.java
rename to domain/src/test/java/nexters/payout/domain/DomainApplicationTests.java
index 21429bb8..169944eb 100644
--- a/domain/src/test/java/nexters/dividend/domain/DomainApplicationTests.java
+++ b/domain/src/test/java/nexters/payout/domain/DomainApplicationTests.java
@@ -1,4 +1,4 @@
-package nexters.dividend.domain;
+package nexters.payout.domain;
 
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
diff --git a/domain/src/test/java/nexters/payout/domain/stock/service/SectorAnalysisServiceTest.java b/domain/src/test/java/nexters/payout/domain/stock/service/SectorAnalysisServiceTest.java
new file mode 100644
index 00000000..af911184
--- /dev/null
+++ b/domain/src/test/java/nexters/payout/domain/stock/service/SectorAnalysisServiceTest.java
@@ -0,0 +1,60 @@
+package nexters.payout.domain.stock.service;
+
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService.SectorInfo;
+import nexters.payout.domain.stock.domain.service.SectorAnalysisService.StockShare;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.within;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+class SectorAnalysisServiceTest {
+
+    @Test
+    void 하나의_티커가_존재하는_경우_섹터비율_검증() {
+        // given
+        Stock stock = StockFixture.createStock(StockFixture.AAPL, Sector.TECHNOLOGY, 3.0);
+        List<StockShare> stockShares = List.of(new StockShare(stock, 1));
+        SectorAnalysisService sectorAnalysisService = new SectorAnalysisService();
+
+        // when
+        Map<Sector, SectorInfo> actual = sectorAnalysisService.calculateSectorRatios(stockShares);
+
+        // then
+        assertAll(
+                () -> assertThat(actual).hasSize(1),
+                () -> assertThat(actual.get(Sector.TECHNOLOGY)).isEqualTo(new SectorInfo(1.0, List.of(new StockShare(stock, 1))))
+        );
+    }
+
+    @Test
+    void 서로_다른_섹터와_개수와_현재가를_가진_2개의_티커가_존재하는_경우_섹터비율_검증() {
+        // given
+        Stock appl = StockFixture.createStock(StockFixture.AAPL, Sector.TECHNOLOGY, 4.0);
+        Stock tsla = StockFixture.createStock(StockFixture.TSLA, Sector.CONSUMER_CYCLICAL, 1.0);
+        List<StockShare> stockShares = List.of(new StockShare(appl, 2), new StockShare(tsla, 1));
+        SectorAnalysisService sectorAnalysisService = new SectorAnalysisService();
+
+        // when
+        Map<Sector, SectorInfo> actual = sectorAnalysisService.calculateSectorRatios(stockShares);
+
+        // then
+        SectorInfo actualFinancialSectorInfo = actual.get(Sector.TECHNOLOGY);
+        SectorInfo actualTechnologySectorInfo = actual.get(Sector.CONSUMER_CYCLICAL);
+
+        assertAll(
+                () -> assertThat(actual).hasSize(2),
+                () -> assertThat(actualFinancialSectorInfo.ratio()).isCloseTo(0.8889, within(0.001)),
+                () -> assertThat(actualFinancialSectorInfo.stockShares()).isEqualTo(List.of(new StockShare(appl, 2))),
+                () -> assertThat(actualTechnologySectorInfo.ratio()).isCloseTo(0.1111, within(0.001)),
+                () -> assertThat(actualTechnologySectorInfo.stockShares()).isEqualTo(List.of(new StockShare(tsla, 1)))
+        );
+    }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/nexters/payout/domain/stock/service/StockDividendAnalysisServiceTest.java b/domain/src/test/java/nexters/payout/domain/stock/service/StockDividendAnalysisServiceTest.java
new file mode 100644
index 00000000..d31b555f
--- /dev/null
+++ b/domain/src/test/java/nexters/payout/domain/stock/service/StockDividendAnalysisServiceTest.java
@@ -0,0 +1,160 @@
+package nexters.payout.domain.stock.service;
+
+import nexters.payout.domain.DividendFixture;
+import nexters.payout.domain.StockFixture;
+import nexters.payout.domain.dividend.domain.Dividend;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+import nexters.payout.domain.stock.domain.service.StockDividendAnalysisService;
+import org.junit.jupiter.api.Test;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.Month;
+import java.time.ZoneId;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static java.time.ZoneOffset.UTC;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class StockDividendAnalysisServiceTest {
+
+    StockDividendAnalysisService stockDividendAnalysisService = new StockDividendAnalysisService();
+
+    @Test
+    void 작년_배당_월_리스트를_정상적으로_반환한다() {
+        // given
+        Stock stock = StockFixture.createStock(StockFixture.AAPL, Sector.TECHNOLOGY);
+        int lastYear = LocalDate.now(UTC).getYear() - 1;
+        Instant janPaymentDate = LocalDate.of(lastYear, 1, 3).atStartOfDay().toInstant(UTC);
+        Instant aprPaymentDate = LocalDate.of(lastYear, 4, 3).atStartOfDay().toInstant(UTC);
+        Instant julPaymentDate = LocalDate.of(lastYear, 7, 3).atStartOfDay().toInstant(UTC);
+        Instant fakePaymentDate = LocalDate.of(LocalDate.now().getYear(), 8, 3).atStartOfDay().toInstant(UTC);
+
+        Dividend janDividend = DividendFixture.createDividendWithExDividendDate(stock.getId(), janPaymentDate);
+        Dividend aprDividend = DividendFixture.createDividendWithExDividendDate(stock.getId(), aprPaymentDate);
+        Dividend julDividend = DividendFixture.createDividendWithExDividendDate(stock.getId(), julPaymentDate);
+        Dividend fakeDividend = DividendFixture.createDividendWithExDividendDate(stock.getId(), fakePaymentDate);
+
+        // when
+        List<Month> actual = stockDividendAnalysisService.calculateDividendMonths(stock, List.of(janDividend, aprDividend, julDividend, fakeDividend));
+
+        // then
+        assertThat(actual).isEqualTo(List.of(Month.JANUARY, Month.APRIL, Month.JULY));
+    }
+
+    @Test
+    void 작년_배당_기록이_없는_경우_빈_리스트를_반환한다() {
+        // given
+        Stock stock = StockFixture.createStock(StockFixture.AAPL, Sector.TECHNOLOGY);
+        Instant fakePaymentDate = LocalDate.of(LocalDate.now().getYear(), 8, 3).atStartOfDay().toInstant(UTC);
+
+        Dividend fakeDividend = DividendFixture.createDividendWithExDividendDate(stock.getId(), fakePaymentDate);
+
+        // when
+        List<Month> actual = stockDividendAnalysisService.calculateDividendMonths(stock, List.of(fakeDividend));
+
+        // then
+        assertThat(actual).isEmpty();
+    }
+
+    @Test
+    void 배당_기록이_없는_경우_빈_리스트를_반환한다() {
+        // given
+        Stock stock = StockFixture.createStock(StockFixture.AAPL, Sector.TECHNOLOGY);
+
+        // when
+        List<Month> actual = stockDividendAnalysisService.calculateDividendMonths(stock, List.of());
+
+        // then
+        assertThat(actual).isEmpty();
+    }
+
+    @Test
+    void 공시된_현재_배당금_지급일이_없는_경우_과거데이터를_기반으로_가까운_지급일을_계산한다() {
+        // given
+        LocalDate now = LocalDate.now();
+
+        Dividend pastDividend = DividendFixture.createDividendWithExDividendDate(
+                UUID.randomUUID(),
+                LocalDate.of(now.getYear() - 1, 1, 10)
+                        .atStartOfDay(ZoneId.systemDefault()).toInstant()
+        );
+
+        Dividend earlistDividend = DividendFixture.createDividendWithExDividendDate(
+                UUID.randomUUID(),
+                LocalDate.of(now.getYear() - 1, 3, 10)
+                        .atStartOfDay(ZoneId.systemDefault()).toInstant()
+        );
+        List<Dividend> lastYearDividends = List.of(pastDividend, earlistDividend);
+
+        // when
+        Optional<Dividend> actual = stockDividendAnalysisService.findUpcomingDividend(lastYearDividends, Collections.emptyList());
+
+        // then
+        assertThat(actual.get()).isEqualTo(earlistDividend);
+    }
+
+    @Test
+    void 공시된_현재_배당금_지급일이_존재하는_경우_실제_지급일을_반환한다() {
+        // given
+        LocalDate now = LocalDate.now();
+        int plusDay = Math.max(now.getDayOfMonth(), now.plusDays(3).getDayOfMonth());
+
+        Dividend lastYearDividend = DividendFixture.createDividend(
+                UUID.randomUUID(),
+                1.0,
+                LocalDate.now().plusDays(10)
+                        .atStartOfDay(ZoneId.systemDefault()).toInstant()
+        );
+
+        Dividend thisYearDividend = DividendFixture.createDividend(
+                UUID.randomUUID(),
+                1.0,
+                LocalDate.now().plusDays(3)
+                        .atStartOfDay(ZoneId.systemDefault()).toInstant()
+        );
+
+        List<Dividend> lastYearDividends = List.of(lastYearDividend);
+        List<Dividend> thisYearDividends = List.of(thisYearDividend);
+
+        // when
+        Optional<Dividend> actual = stockDividendAnalysisService.findUpcomingDividend(lastYearDividends, thisYearDividends);
+
+        // then
+        assertThat(actual.get()).isEqualTo(thisYearDividend);
+    }
+
+    @Test
+    void 배당수익률을_구할수있다() {
+        // given
+        Stock aapl = StockFixture.createStock(StockFixture.AAPL, Sector.TECHNOLOGY, 40.0);
+        List<Dividend> dividends = List.of(DividendFixture.createDividendWithDividend(UUID.randomUUID(), 10.0),
+                DividendFixture.createDividendWithDividend(UUID.randomUUID(), 20.0)
+        );
+        Double expected = 30.0 / 40.0;
+
+        // when
+        Double actual = stockDividendAnalysisService.calculateDividendYield(aapl, dividends);
+
+        // then
+        assertThat(actual).isEqualTo(expected);
+    }
+
+    @Test
+    void 배당금_리스트로부터_평균_배당금을_구할수있다() {
+        // given
+        List<Dividend> dividends = List.of(DividendFixture.createDividendWithDividend(UUID.randomUUID(), 10.0),
+                DividendFixture.createDividendWithDividend(UUID.randomUUID(), 20.0)
+        );
+
+        // when
+        Double actual = stockDividendAnalysisService.calculateAverageDividend(dividends);
+
+        // then
+        assertThat(actual).isEqualTo(15.0);
+    }
+}
\ No newline at end of file
diff --git a/domain/src/testFixtures/java/nexters/payout/domain/DividendFixture.java b/domain/src/testFixtures/java/nexters/payout/domain/DividendFixture.java
new file mode 100644
index 00000000..606afe99
--- /dev/null
+++ b/domain/src/testFixtures/java/nexters/payout/domain/DividendFixture.java
@@ -0,0 +1,70 @@
+package nexters.payout.domain;
+
+import nexters.payout.domain.dividend.domain.Dividend;
+
+import java.time.Instant;
+import java.util.UUID;
+
+public class DividendFixture {
+
+    public static Dividend createDividendWithDividend(UUID stockId, Double dividend) {
+        return new Dividend(
+                UUID.randomUUID(),
+                stockId,
+                dividend,
+                Instant.now(),
+                Instant.now(),
+                Instant.now()
+        );
+    }
+
+    public static Dividend createDividendWithExDividendDate(UUID stockId, Instant exDividendDate) {
+        return new Dividend(
+                UUID.randomUUID(),
+                stockId,
+                12.21,
+                exDividendDate,
+                Instant.parse("2023-12-21T00:00:00Z"),
+                Instant.parse("2023-12-22T00:00:00Z"));
+    }
+
+    public static Dividend createDividendWithPaymentDate(UUID stockId, Double dividend, Instant paymentDate) {
+        return new Dividend(
+                UUID.randomUUID(),
+                stockId,
+                dividend,
+                paymentDate,
+                paymentDate,
+                paymentDate);
+    }
+
+    public static Dividend createDividend(UUID stockId, Double dividend, Instant exDividendDate) {
+        return new Dividend(
+                UUID.randomUUID(),
+                stockId,
+                dividend,
+                exDividendDate,
+                exDividendDate,
+                exDividendDate);
+    }
+
+    public static Dividend createDividend(UUID stockId) {
+        return new Dividend(
+                UUID.randomUUID(),
+                stockId,
+                12.21,
+                Instant.parse("2023-12-21T00:00:00Z"),
+                Instant.parse("2023-12-23T00:00:00Z"),
+                Instant.parse("2023-12-22T00:00:00Z"));
+    }
+
+    public static Dividend createDividendWithNullDate(UUID stockId) {
+        return new Dividend(
+                UUID.randomUUID(),
+                stockId,
+                12.21,
+                Instant.parse("2023-12-21T00:00:00Z"),
+                null,
+                null);
+    }
+}
diff --git a/domain/src/testFixtures/java/nexters/payout/domain/StockFixture.java b/domain/src/testFixtures/java/nexters/payout/domain/StockFixture.java
new file mode 100644
index 00000000..a062bd0d
--- /dev/null
+++ b/domain/src/testFixtures/java/nexters/payout/domain/StockFixture.java
@@ -0,0 +1,29 @@
+package nexters.payout.domain;
+
+import nexters.payout.domain.stock.domain.Exchange;
+import nexters.payout.domain.stock.domain.Sector;
+import nexters.payout.domain.stock.domain.Stock;
+
+import java.util.UUID;
+
+public class StockFixture {
+    public static final String TSLA = "TSLA";
+    public static final String AAPL = "AAPL";
+    public static final String SBUX = "SBUX";
+
+    public static Stock createStock(String ticker, Double price, Integer volume) {
+        return new Stock(ticker, "tesla", Sector.FINANCIAL_SERVICES, Exchange.NYSE.name(), "industry", price, volume, "");
+    }
+
+    public static Stock createStock(String ticker, Sector sector) {
+        return new Stock(UUID.randomUUID(), ticker, ticker, sector, Exchange.NYSE.name(), "industry", 0.0, 0, "");
+    }
+
+    public static Stock createStock(String ticker, String companyName) {
+        return new Stock(UUID.randomUUID(), ticker, companyName, Sector.TECHNOLOGY, Exchange.NYSE.name(), "industry", 0.0, 0, "");
+    }
+
+    public static Stock createStock(String ticker, Sector sector, Double price) {
+        return new Stock(UUID.randomUUID(), ticker, ticker, sector, Exchange.NYSE.name(), "industry", price, 0, "");
+    }
+}
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
new file mode 100644
index 00000000..dba09173
--- /dev/null
+++ b/nginx/nginx.conf
@@ -0,0 +1,36 @@
+user nginx;
+worker_processes  auto;
+
+error_log  /var/log/nginx/error.log warn;
+pid        /var/run/nginx.pid;
+
+events {
+    worker_connections 1024;
+}
+
+http {
+    include       /etc/nginx/mime.types;
+    sendfile on;
+    keepalive_timeout 65;
+
+    server {
+      listen 80;
+      # listen [::]:80;
+
+      if ($http_x_forwarded_proto != 'https'){
+        return 301 https://$host$request_uri;
+      }
+
+      location /health {
+        return 200 'ok';
+        add_header Content-Type text/plain;
+      }
+
+      location / {
+        proxy_pass http://green-api:8080;
+        proxy_set_header    Host                $http_host;
+        proxy_set_header    X-Real-IP           $remote_addr;
+        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
+      }
+    }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 8578c629..0888352c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,4 @@
-rootProject.name = 'dividend-server'
+rootProject.name = 'payout-server'
 
 include(":api-server")
 include(":batch")